diff --git a/Cargo.lock b/Cargo.lock index 2123179..7bf332e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -181,7 +181,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.38", ] [[package]] @@ -298,6 +298,101 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "getrandom" version = "0.2.10" @@ -401,6 +496,18 @@ dependencies = [ "libm", ] +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -588,6 +695,41 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316" +[[package]] +name = "rstest" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de1bb486a691878cd320c2f0d319ba91eeaa2e894066d8b5f8f117c000e9d962" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290ca1a1c8ca7edb7c3283bd44dc35dd54fdec6253a3912e201ba1072018fca8" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", + "unicode-ident", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.20" @@ -635,11 +777,18 @@ dependencies = [ "rayon", "regex", "regex-automata", + "rstest", "tempfile", "thiserror", "unescape", ] +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + [[package]] name = "serde" version = "1.0.189" @@ -657,7 +806,7 @@ checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.38", ] [[package]] @@ -666,12 +815,32 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2aeaf503862c419d66959f5d7ca015337d864e9c49485d771b732e2a20453597" +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.38" @@ -729,7 +898,7 @@ checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.38", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index e44aede..8c49877 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ console = "0.15.7" insta = "1.34.0" ansi-to-html = "0.1.3" regex-automata = "0.4.3" +rstest = "0.17.0" [profile.release] opt-level = 3 diff --git a/src/cli.rs b/src/cli.rs index e96e609..9a4eeac 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -64,6 +64,10 @@ w - match full words only /// use captured values like $1, $2, etc. pub replace_with: String, + #[arg(long)] + /// Overwrite file in place instead of creating tmp file and swapping atomically + pub in_place: bool, + /// The path to file(s). This is optional - sd can also read from STDIN. /// /// Note: sd modifies files in-place by default. See documentation for diff --git a/src/main.rs b/src/main.rs index c346ab6..b7b493f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -91,7 +91,12 @@ fn try_main() -> Result<()> { for (source, replaced) in sources.iter().zip(replaced) { match source { Source::File(path) => { - if let Err(e) = write_with_temp(path, &replaced) { + let result = if !options.in_place { + write_with_temp(path, &replaced) + } else { + write_in_place(path, &replaced) + }; + if let Err(e) = result { failed_jobs.push((path.to_owned(), e)); } } @@ -130,3 +135,13 @@ fn write_with_temp(path: &PathBuf, data: &[u8]) -> Result<()> { Ok(()) } + +fn write_in_place(path: &PathBuf, data: &[u8]) -> Result<()> { + let path = fs::canonicalize(path)?; + + let mut source = fs::OpenOptions::new().write(true).open(path)?; + source.write_all(&data)?; + source.set_len(data.len() as u64)?; + + Ok(()) +} diff --git a/tests/cli.rs b/tests/cli.rs index 1030dc1..5d06726 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -3,6 +3,7 @@ mod cli { use anyhow::Result; use assert_cmd::Command; + use rstest::rstest; use std::{fs, io::prelude::*, path::Path}; fn sd() -> Command { @@ -29,29 +30,37 @@ mod cli { Ok(()) } - #[test] - fn in_place() -> Result<()> { + #[rstest] + fn in_place(#[values(false, true)] no_swap: bool) -> Result<()> { let mut file = tempfile::NamedTempFile::new()?; file.write_all(b"abc123def")?; let path = file.into_temp_path(); - sd().args(["abc\\d+", "", path.to_str().unwrap()]) - .assert() - .success(); + let mut cmd = sd(); + cmd.args(["abc\\d+", "", path.to_str().unwrap()]); + if no_swap { + cmd.arg("--in-place"); + } + cmd.assert().success(); assert_file(&path, "def"); Ok(()) } - #[test] - fn in_place_with_empty_result_file() -> Result<()> { + #[rstest] + fn in_place_with_empty_result_file( + #[values(false, true)] no_swap: bool, + ) -> Result<()> { let mut file = tempfile::NamedTempFile::new()?; file.write_all(b"a7c")?; let path = file.into_temp_path(); - sd().args(["a\\dc", "", path.to_str().unwrap()]) - .assert() - .success(); + let mut cmd = sd(); + cmd.args(["a\\dc", "", path.to_str().unwrap()]); + if no_swap { + cmd.arg("--in-place"); + } + cmd.assert().success(); assert_file(&path, ""); Ok(()) @@ -61,8 +70,10 @@ mod cli { target_family = "windows", ignore = "Windows symlinks are privileged" )] - #[test] - fn in_place_following_symlink() -> Result<()> { + #[rstest] + fn in_place_following_symlink( + #[values(false, true)] no_swap: bool, + ) -> Result<()> { let dir = tempfile::tempdir()?; let path = dir.path(); let file = path.join("file"); @@ -71,9 +82,12 @@ mod cli { create_soft_link(&file, &link)?; std::fs::write(&file, "abc123def")?; - sd().args(["abc\\d+", "", link.to_str().unwrap()]) - .assert() - .success(); + let mut cmd = sd(); + cmd.args(["abc\\d+", "", link.to_str().unwrap()]); + if no_swap { + cmd.arg("--in-place"); + } + cmd.assert().success(); assert_file(&file, "def"); assert!(std::fs::symlink_metadata(link)?.file_type().is_symlink()); @@ -81,25 +95,31 @@ mod cli { Ok(()) } - #[test] - fn replace_into_stdout() -> Result<()> { + #[rstest] + fn replace_into_stdout(#[values(false, true)] no_swap: bool) -> Result<()> { let mut file = tempfile::NamedTempFile::new()?; file.write_all(b"abc123def")?; - sd().args(["-p", "abc\\d+", "", file.path().to_str().unwrap()]) - .assert() - .success() - .stdout("def"); + let mut cmd = sd(); + cmd.args(["-p", "abc\\d+", "", file.path().to_str().unwrap()]); + if no_swap { + cmd.arg("--in-place"); + } + cmd.assert().success().stdout("def"); assert_file(file.path(), "abc123def"); Ok(()) } - #[test] - fn stdin() -> Result<()> { - sd().args(["abc\\d+", ""]) - .write_stdin("abc123def") + #[rstest] + fn stdin(#[values(false, true)] no_swap: bool) -> Result<()> { + let mut cmd = sd(); + cmd.args(["abc\\d+", ""]); + if no_swap { + cmd.arg("--in-place"); + } + cmd.write_stdin("abc123def") .assert() .success() .stdout("def");