From cd461fa5a5353b8294b879953e7a6aa75366f9dd Mon Sep 17 00:00:00 2001 From: figsoda Date: Thu, 14 Oct 2021 15:22:48 -0400 Subject: [PATCH 01/36] apply clippy lints and small refactors (#86) --- src/commands.rs | 8 ++++---- src/error.rs | 16 ++++++---------- src/lib.rs | 2 +- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index bf278b6..24847bc 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -29,7 +29,7 @@ const BUFFER_CAPACITY: usize = 1024 * 64; fn represents_several_files(files: &[PathBuf]) -> bool { let is_non_empty_dir = |path: &PathBuf| { - let is_non_empty = || !dir_is_empty(&path); + let is_non_empty = || !dir_is_empty(path); path.is_dir().then(is_non_empty).unwrap_or_default() }; @@ -51,7 +51,7 @@ pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> { .hint("Examples:") .hint(format!(" ouch compress ... {}.tar.gz", to_utf(&output_path))) .hint(format!(" ouch compress ... {}.zip", to_utf(&output_path))) - .into_owned(); + .clone(); return Err(Error::with_reason(reason)); } @@ -80,7 +80,7 @@ pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> { .hint(format!("Try inserting '.tar' or '.zip' before '{}'.", &formats[0])) .hint(format!("From: {}", output_path)) .hint(format!(" To : {}", suggested_output_path)) - .into_owned(); + .clone(); return Err(Error::with_reason(reason)); } @@ -91,7 +91,7 @@ pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> { .detail(format!("{} can only be used at the start of the file extension.", format)) .hint(format!("If you wish to compress multiple files, start the extension with {}.", format)) .hint(format!("Otherwise, remove {} from '{}'.", format, to_utf(&output_path))) - .into_owned(); + .clone(); return Err(Error::with_reason(reason)); } diff --git a/src/error.rs b/src/error.rs index 3792bfb..caf7e6c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -79,10 +79,6 @@ impl FinalError { self.hints.push(hint.to_string()); self } - - pub fn into_owned(&mut self) -> Self { - std::mem::take(self) - } } impl fmt::Display for Error { @@ -93,7 +89,7 @@ impl fmt::Display for Error { .detail("Ouch could not detect the compression format") .hint("Use a supported format extension, like '.zip' or '.tar.gz'") .hint("Check https://github.com/vrmiguel/ouch for a full list of supported formats") - .into_owned(); + .clone(); error } @@ -111,7 +107,7 @@ impl fmt::Display for Error { let error = FinalError::with_title("It seems you're trying to compress the root folder.") .detail("This is unadvisable since ouch does compressions in-memory.") .hint("Use a more appropriate tool for this, such as rsync.") - .into_owned(); + .clone(); error } @@ -123,7 +119,7 @@ impl fmt::Display for Error { .hint(" - The output argument.") .hint("") .hint("Example: `ouch compress image.png img.zip`") - .into_owned(); + .clone(); error } @@ -134,7 +130,7 @@ impl fmt::Display for Error { .hint(" - At least one input argument.") .hint("") .hint("Example: `ouch decompress imgs.tar.gz`") - .into_owned(); + .clone(); error } @@ -144,7 +140,7 @@ impl fmt::Display for Error { .detail("It's probably our fault") .detail("Please help us improve by reporting the issue at:") .detail(format!(" {}https://github.com/vrmiguel/ouch/issues ", cyan())) - .into_owned(); + .clone(); error } @@ -152,7 +148,7 @@ impl fmt::Display for Error { Error::IoError { reason } => FinalError::with_title(reason), Error::CompressionTypo => FinalError::with_title("Possible typo detected") .hint(format!("Did you mean '{}ouch compress{}'?", magenta(), reset())) - .into_owned(), + .clone(), Error::UnknownExtensionError(_) => todo!(), Error::AlreadyExists => todo!(), Error::InvalidZipArchive(_) => todo!(), diff --git a/src/lib.rs b/src/lib.rs index b6fd3b7..52537a8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,7 +24,7 @@ use lazy_static::lazy_static; /// The status code ouch has when an error is encountered pub const EXIT_FAILURE: i32 = libc::EXIT_FAILURE; -const VERSION: &'static str = env!("CARGO_PKG_VERSION"); +const VERSION: &str = env!("CARGO_PKG_VERSION"); lazy_static! { static ref NO_COLOR_IS_SET: bool = { From f923423a06a2ed68c4f3a249ecfe4fd992488282 Mon Sep 17 00:00:00 2001 From: figsoda Date: Thu, 14 Oct 2021 15:55:34 -0400 Subject: [PATCH 02/36] Extension: add support for tgz (#85) * extension: add support for tgz --- README.md | 2 +- src/commands.rs | 33 ++++++++++++++++++++++++++------ src/extension.rs | 25 ++++++++++++------------ tests/compress_and_decompress.rs | 4 +++- 4 files changed, 44 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 4c70fae..cea0c4c 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ For compiling, check [the wiki guide](https://github.com/ouch-org/ouch/wiki/Comp ## Supported formats -| | .tar | .zip | .bz, .bz2 | .gz | .xz, .lz, .lzma | .zst | +| | .tar, .tgz | .zip | .bz, .bz2 | .gz | .xz, .lz, .lzma | .zst | |:-------------:|:----:|:----:|:---------:| --- |:---------------:| --- | | Decompression | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | Compression | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | diff --git a/src/commands.rs b/src/commands.rs index 24847bc..3ae5cd9 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -170,14 +170,23 @@ fn compress_files( let file_writer = BufWriter::with_capacity(BUFFER_CAPACITY, output_file); if formats.len() == 1 { - let build_archive_from_paths = match formats[0] { - Tar => archive::tar::build_archive_from_paths, - Zip => archive::zip::build_archive_from_paths, + match formats[0] { + Tar => { + let mut bufwriter = archive::tar::build_archive_from_paths(&files, file_writer)?; + bufwriter.flush()?; + } + Tgz => { + // Wrap it into an gz_decoder, and pass to the tar archive builder + let gz_decoder = flate2::write::GzEncoder::new(file_writer, Default::default()); + let mut bufwriter = archive::tar::build_archive_from_paths(&files, gz_decoder)?; + bufwriter.flush()?; + } + Zip => { + let mut bufwriter = archive::zip::build_archive_from_paths(&files, file_writer)?; + bufwriter.flush()?; + } _ => unreachable!(), }; - - let mut bufwriter = build_archive_from_paths(&files, file_writer)?; - bufwriter.flush()?; } else { let mut writer: Box = Box::new(file_writer); @@ -213,6 +222,12 @@ fn compress_files( let mut writer = archive::tar::build_archive_from_paths(&files, writer)?; writer.flush()?; } + Tgz => { + // Wrap it into an gz_decoder, and pass to the tar archive builder + let gz_decoder = flate2::write::GzEncoder::new(writer, Default::default()); + let mut writer = archive::tar::build_archive_from_paths(&files, gz_decoder)?; + writer.flush()?; + } Zip => { eprintln!("{yellow}Warning:{reset}", yellow = colors::yellow(), reset = colors::reset()); eprintln!("\tCompressing .zip entirely in memory."); @@ -305,6 +320,12 @@ fn decompress_file( let _ = crate::archive::tar::unpack_archive(reader, output_folder, flags)?; info!("Successfully uncompressed archive in '{}'.", to_utf(output_folder)); } + Tgz => { + utils::create_dir_if_non_existent(output_folder)?; + let reader = chain_reader_decoder(&Gzip, reader)?; + let _ = crate::archive::tar::unpack_archive(reader, output_folder, flags)?; + info!("Successfully uncompressed archive in '{}'.", to_utf(output_folder)); + } Zip => { utils::create_dir_if_non_existent(output_folder)?; diff --git a/src/extension.rs b/src/extension.rs index 5f33b58..aa2777a 100644 --- a/src/extension.rs +++ b/src/extension.rs @@ -1,6 +1,6 @@ //! Our representation of all the supported compression formats. -use std::{fmt, path::Path}; +use std::{ffi::OsStr, fmt, path::Path}; use self::CompressionFormat::*; @@ -11,6 +11,7 @@ pub enum CompressionFormat { Bzip, // .bz Lzma, // .lzma Tar, // .tar (technically not a compression extension, but will do for now) + Tgz, // .tgz Zstd, // .zst Zip, // .zip } @@ -26,6 +27,7 @@ impl fmt::Display for CompressionFormat { Zstd => ".zst", Lzma => ".lz", Tar => ".tar", + Tgz => ".tgz", Zip => ".zip", } ) @@ -44,18 +46,17 @@ pub fn separate_known_extensions_from_name(mut path: &Path) -> (&Path, Vec Tar, - _ if extension == "zip" => Zip, - _ if extension == "bz" || extension == "bz2" => Bzip, - _ if extension == "gz" => Gzip, - _ if extension == "xz" || extension == "lzma" || extension == "lz" => Lzma, - _ if extension == "zst" => Zstd, + while let Some(extension) = path.extension().and_then(OsStr::to_str) { + extensions.push(match extension { + "tar" => Tar, + "tgz" => Tgz, + "zip" => Zip, + "bz" | "bz2" => Bzip, + "gz" => Gzip, + "xz" | "lzma" | "lz" => Lzma, + "zst" => Zstd, _ => break, - }; - - extensions.push(extension); + }); // Update for the next iteration path = if let Some(stem) = path.file_stem() { Path::new(stem) } else { Path::new("") }; diff --git a/tests/compress_and_decompress.rs b/tests/compress_and_decompress.rs index 22b99fc..e97ab33 100644 --- a/tests/compress_and_decompress.rs +++ b/tests/compress_and_decompress.rs @@ -27,12 +27,13 @@ fn sanity_check_through_mime() { let bytes = generate_random_file_content(&mut SmallRng::from_entropy()); test_file.write_all(&bytes).expect("to successfully write bytes to the file"); - let formats = ["tar", "zip", "tar.gz", "tar.bz", "tar.bz2", "tar.lzma", "tar.xz", "tar.zst"]; + let formats = ["tar", "zip", "tar.gz", "tgz", "tar.bz", "tar.bz2", "tar.lzma", "tar.xz", "tar.zst"]; let expected_mimes = [ "application/x-tar", "application/zip", "application/gzip", + "application/gzip", "application/x-bzip2", "application/x-bzip2", "application/x-xz", @@ -67,6 +68,7 @@ fn test_each_format() { test_compressing_and_decompressing_archive("tar.lz"); test_compressing_and_decompressing_archive("tar.lzma"); test_compressing_and_decompressing_archive("tar.zst"); + test_compressing_and_decompressing_archive("tgz"); test_compressing_and_decompressing_archive("zip"); test_compressing_and_decompressing_archive("zip.gz"); test_compressing_and_decompressing_archive("zip.bz"); From 1c24f414946f098167fd6961bc4be4edb8267255 Mon Sep 17 00:00:00 2001 From: Santo Cariotti Date: Thu, 14 Oct 2021 22:18:46 +0200 Subject: [PATCH 03/36] chore: print format type instead of index (#84) When it raises an error caused by position of the format, now prints the format type as string instead of the position inside the array of formats. In this way you can read on stdout the type like `.tar` or `.lz` instead of `1`, `2`, .., `n`. --- src/commands.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 3ae5cd9..0850039 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -85,15 +85,15 @@ pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> { return Err(Error::with_reason(reason)); } - if let Some(format) = formats.iter().skip(1).position(|format| matches!(format, Tar | Zip)) { + if let Some(format) = formats.iter().skip(1).find(|format| matches!(format, Tar | Zip)) { let reason = FinalError::with_title(format!("Cannot compress to '{}'.", to_utf(&output_path))) .detail(format!("Found the format '{}' in an incorrect position.", format)) - .detail(format!("{} can only be used at the start of the file extension.", format)) - .hint(format!("If you wish to compress multiple files, start the extension with {}.", format)) - .hint(format!("Otherwise, remove {} from '{}'.", format, to_utf(&output_path))) + .detail(format!("'{}' can only be used at the start of the file extension.", format)) + .hint(format!("If you wish to compress multiple files, start the extension with '{}'.", format)) + .hint(format!("Otherwise, remove the last '{}' from '{}'.", format, to_utf(&output_path))) .clone(); - return Err(Error::with_reason(reason)); + return Err(Error::with_reason(reason)); } if output_path.exists() && !utils::user_wants_to_overwrite(&output_path, flags)? { From c89c34a91f3df9841558391020cfc40d770f4e1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Marcos=20Bezerra?= Date: Thu, 14 Oct 2021 18:17:52 -0300 Subject: [PATCH 04/36] Fix single format compression (#89) Now working for formats that are not archives, like file.gz and file.xz --- src/commands.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 0850039..05db7a6 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -93,7 +93,7 @@ pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> { .hint(format!("Otherwise, remove the last '{}' from '{}'.", format, to_utf(&output_path))) .clone(); - return Err(Error::with_reason(reason)); + return Err(Error::with_reason(reason)); } if output_path.exists() && !utils::user_wants_to_overwrite(&output_path, flags)? { @@ -169,7 +169,7 @@ fn compress_files( ) -> crate::Result<()> { let file_writer = BufWriter::with_capacity(BUFFER_CAPACITY, output_file); - if formats.len() == 1 { + if let [Tar | Tgz | Zip] = *formats.as_slice() { match formats[0] { Tar => { let mut bufwriter = archive::tar::build_archive_from_paths(&files, file_writer)?; From 9907ebcf364049fcab6ef038ec4d72ab1fcc2671 Mon Sep 17 00:00:00 2001 From: Spyros Roum Date: Fri, 15 Oct 2021 00:33:17 +0300 Subject: [PATCH 05/36] Properly detect if we are compressing a partially compressed file --- src/commands.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/commands.rs b/src/commands.rs index 05db7a6..182261e 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -163,12 +163,24 @@ pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> { fn compress_files( files: Vec, - formats: Vec, + mut formats: Vec, output_file: fs::File, _flags: &oof::Flags, ) -> crate::Result<()> { let file_writer = BufWriter::with_capacity(BUFFER_CAPACITY, output_file); + if files.len() == 1 { + // It's possible the file is already partially compressed so we don't want to compress it again + // `ouch compress file.tar.gz file.tar.gz.xz` should produce `file.tar.gz.xz` and not `file.tar.gz.tar.gz.xz` + let cur_extensions = extension::extensions_from_path(&files[0]); + + // If the input is a subset at the start of `formats` then remove the extensions + if cur_extensions.len() < formats.len() && cur_extensions.iter().zip(&formats).all(|(inp, out)| inp == out) { + let drain_iter = formats.drain(..cur_extensions.len()); + drop(drain_iter); // Remove the extensions from `formats` + } + } + if let [Tar | Tgz | Zip] = *formats.as_slice() { match formats[0] { Tar => { From 69e5e3291cf6d90d79698d17c231be8217306432 Mon Sep 17 00:00:00 2001 From: figsoda Date: Thu, 14 Oct 2021 19:00:40 -0400 Subject: [PATCH 06/36] Fix single file decompression with specified output directory (#93) --- src/commands.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 05db7a6..c8289fa 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -305,6 +305,8 @@ fn decompress_file( reader = chain_reader_decoder(format, reader)?; } + utils::create_dir_if_non_existent(output_folder)?; + match formats[0] { Gzip | Bzip | Lzma | Zstd => { reader = chain_reader_decoder(&formats[0], reader)?; @@ -316,19 +318,15 @@ fn decompress_file( info!("Successfully uncompressed archive in '{}'.", to_utf(output_path)); } Tar => { - utils::create_dir_if_non_existent(output_folder)?; let _ = crate::archive::tar::unpack_archive(reader, output_folder, flags)?; info!("Successfully uncompressed archive in '{}'.", to_utf(output_folder)); } Tgz => { - utils::create_dir_if_non_existent(output_folder)?; let reader = chain_reader_decoder(&Gzip, reader)?; let _ = crate::archive::tar::unpack_archive(reader, output_folder, flags)?; info!("Successfully uncompressed archive in '{}'.", to_utf(output_folder)); } Zip => { - utils::create_dir_if_non_existent(output_folder)?; - eprintln!("Compressing first into .zip."); eprintln!("Warning: .zip archives with extra extensions have a downside."); eprintln!( From 16fbebe8fe9b1d771ebd6c2d4ce5b3fb5f5b7e8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Marcos=20Bezerra?= Date: Thu, 14 Oct 2021 20:12:42 -0300 Subject: [PATCH 07/36] Updating Cargo.lock to newer dependencies (#92) bitflags v1.2.1 -> v1.3.2 cc v1.0.69 -> v1.0.71 filetime v0.2.14 -> v0.2.15 pkg-config v0.3.19 -> v0.3.20 ppv-lite86 v0.2.10 -> v0.2.14 proc-macro2 v1.0.28 -> v1.0.30 quote v1.0.9 -> v1.0.10 redox_syscall v0.2.9 -> v0.2.10 syn v1.0.74 -> v1.0.80 thiserror v1.0.26 -> v1.0.30 thiserror-impl v1.0.26 -> v1.0.30 --- Cargo.lock | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cc1dcff..2a9d63a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,9 +27,9 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" [[package]] name = "bitflags" -version = "1.2.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "byteorder" @@ -60,9 +60,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.69" +version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" +checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd" dependencies = [ "jobserver", ] @@ -94,9 +94,9 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8" +checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98" dependencies = [ "cfg-if", "libc", @@ -221,30 +221,30 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" +checksum = "7c9b1041b4387893b91ee6746cddfc28516aff326a3519fb2adf820932c5e6cb" [[package]] name = "ppv-lite86" -version = "0.2.10" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" +checksum = "c3ca011bd0129ff4ae15cd04c4eef202cadf6c51c21e47aba319b4e0501db741" [[package]] name = "proc-macro2" -version = "1.0.28" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612" +checksum = "edc3358ebc67bc8b7fa0c007f945b0b18226f78437d61bec735a9eb96b61ee70" dependencies = [ "unicode-xid", ] [[package]] name = "quote" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" dependencies = [ "proc-macro2", ] @@ -291,9 +291,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" dependencies = [ "bitflags", ] @@ -324,9 +324,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "1.0.74" +version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c" +checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194" dependencies = [ "proc-macro2", "quote", @@ -360,18 +360,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.26" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.26" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" dependencies = [ "proc-macro2", "quote", From 123ccddd91cd508512752007dbc3e5e3f488ab4a Mon Sep 17 00:00:00 2001 From: Spyros Roum Date: Fri, 15 Oct 2021 02:44:15 +0300 Subject: [PATCH 08/36] Move the check to `run` function --- src/commands.rs | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 182261e..ae46e5a 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -41,7 +41,7 @@ pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> { match command { Command::Compress { files, output_path } => { // Formats from path extension, like "file.tar.gz.xz" -> vec![Tar, Gzip, Lzma] - let formats = extension::extensions_from_path(&output_path); + let mut formats = extension::extensions_from_path(&output_path); if formats.is_empty() { let reason = FinalError::with_title(format!("Cannot compress to '{}'.", to_utf(&output_path))) @@ -57,7 +57,7 @@ pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> { } if matches!(&formats[0], Bzip | Gzip | Lzma) && represents_several_files(&files) { - // This piece of code creates a sugestion for compressing multiple files + // This piece of code creates a suggestion for compressing multiple files // It says: // Change from file.bz.xz // To file.tar.bz.xz @@ -102,6 +102,22 @@ pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> { } let output_file = fs::File::create(&output_path)?; + + if files.len() == 1 { + // It's possible the file is already partially compressed so we don't want to compress it again + // `ouch compress file.tar.gz file.tar.gz.xz` should produce `file.tar.gz.xz` and not `file.tar.gz.tar.gz.xz` + let input_extensions = extension::extensions_from_path(&files[0]); + + // If the input is a sublist at the start of `formats` then remove the extensions + // Note: If input_extensions is empty this counts as true + if !input_extensions.is_empty() + && input_extensions.len() < formats.len() + && input_extensions.iter().zip(&formats).all(|(inp, out)| inp == out) + { + let drain_iter = formats.drain(..input_extensions.len()); + drop(drain_iter); // Remove the extensions from `formats` + } + } let compress_result = compress_files(files, formats, output_file, flags); // If any error occurred, delete incomplete file @@ -163,24 +179,12 @@ pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> { fn compress_files( files: Vec, - mut formats: Vec, + formats: Vec, output_file: fs::File, _flags: &oof::Flags, ) -> crate::Result<()> { let file_writer = BufWriter::with_capacity(BUFFER_CAPACITY, output_file); - if files.len() == 1 { - // It's possible the file is already partially compressed so we don't want to compress it again - // `ouch compress file.tar.gz file.tar.gz.xz` should produce `file.tar.gz.xz` and not `file.tar.gz.tar.gz.xz` - let cur_extensions = extension::extensions_from_path(&files[0]); - - // If the input is a subset at the start of `formats` then remove the extensions - if cur_extensions.len() < formats.len() && cur_extensions.iter().zip(&formats).all(|(inp, out)| inp == out) { - let drain_iter = formats.drain(..cur_extensions.len()); - drop(drain_iter); // Remove the extensions from `formats` - } - } - if let [Tar | Tgz | Zip] = *formats.as_slice() { match formats[0] { Tar => { From aa65743e4ebdfe8c5b96aaef53bdafd4715e81df Mon Sep 17 00:00:00 2001 From: Spyros Roum Date: Fri, 15 Oct 2021 02:44:34 +0300 Subject: [PATCH 09/36] Add some `info!` for the user --- src/commands.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/commands.rs b/src/commands.rs index ae46e5a..1e221ef 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -114,6 +114,11 @@ pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> { && input_extensions.len() < formats.len() && input_extensions.iter().zip(&formats).all(|(inp, out)| inp == out) { + info!( + "Partial compression detected. Compressing {} into {}", + to_utf(files[0].as_path().file_name().unwrap()), + to_utf(&output_path) + ); let drain_iter = formats.drain(..input_extensions.len()); drop(drain_iter); // Remove the extensions from `formats` } From baf23fa685922bf7a16d3ace76b43ce763914e1b Mon Sep 17 00:00:00 2001 From: Spyros Roum Date: Fri, 15 Oct 2021 02:45:07 +0300 Subject: [PATCH 10/36] Use `represents_several_files` instead of checking len of `files` --- src/commands.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands.rs b/src/commands.rs index 1e221ef..32c60fc 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -103,7 +103,7 @@ pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> { let output_file = fs::File::create(&output_path)?; - if files.len() == 1 { + if !represents_several_files(&files) { // It's possible the file is already partially compressed so we don't want to compress it again // `ouch compress file.tar.gz file.tar.gz.xz` should produce `file.tar.gz.xz` and not `file.tar.gz.tar.gz.xz` let input_extensions = extension::extensions_from_path(&files[0]); From 161b8d0d66ef63d44ba682ee693b9a57b7d7c68f Mon Sep 17 00:00:00 2001 From: figsoda Date: Thu, 14 Oct 2021 21:25:57 -0400 Subject: [PATCH 11/36] refactor: better NO_COLOR support --- Cargo.lock | 14 +++++------ Cargo.toml | 2 +- src/commands.rs | 6 ++--- src/dialogs.rs | 2 +- src/error.rs | 12 ++++----- src/lib.rs | 24 ++++++------------ src/macros.rs | 10 ++------ src/utils.rs | 67 +++++++++++++++++-------------------------------- 8 files changed, 50 insertions(+), 87 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2a9d63a..a800ba3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -155,12 +155,6 @@ dependencies = [ "libc", ] -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - [[package]] name = "libc" version = "0.2.103" @@ -199,6 +193,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "once_cell" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" + [[package]] name = "ouch" version = "0.2.0" @@ -207,8 +207,8 @@ dependencies = [ "bzip2", "flate2", "infer", - "lazy_static", "libc", + "once_cell", "rand", "strsim", "tar", diff --git a/Cargo.toml b/Cargo.toml index f1d3a9f..071672a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ description = "A command-line utility for easily compressing and decompressing f [dependencies] atty = "0.2.14" -lazy_static = "1.4.0" +once_cell = "1.8.0" walkdir = "2.3.2" strsim = "0.10.0" bzip2 = "0.4.3" diff --git a/src/commands.rs b/src/commands.rs index c8289fa..11e345a 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -109,10 +109,10 @@ pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> { // Print an extra alert message pointing out that we left a possibly // CORRUPTED FILE at `output_path` if let Err(err) = fs::remove_file(&output_path) { - eprintln!("{red}FATAL ERROR:\n", red = colors::red()); + eprintln!("{red}FATAL ERROR:\n", red = *colors::RED); eprintln!(" Please manually delete '{}'.", to_utf(&output_path)); eprintln!(" Compression failed and we could not delete '{}'.", to_utf(&output_path),); - eprintln!(" Error:{reset} {}{red}.{reset}\n", err, reset = colors::reset(), red = colors::red()); + eprintln!(" Error:{reset} {}{red}.{reset}\n", err, reset = *colors::RESET, red = *colors::RED); } } else { info!("Successfully compressed '{}'.", to_utf(output_path)); @@ -229,7 +229,7 @@ fn compress_files( writer.flush()?; } Zip => { - eprintln!("{yellow}Warning:{reset}", yellow = colors::yellow(), reset = colors::reset()); + eprintln!("{yellow}Warning:{reset}", yellow = *colors::YELLOW, reset = *colors::RESET); eprintln!("\tCompressing .zip entirely in memory."); eprintln!("\tIf the file is too big, your PC might freeze!"); eprintln!( diff --git a/src/dialogs.rs b/src/dialogs.rs index 95db53f..4f4d7b2 100644 --- a/src/dialogs.rs +++ b/src/dialogs.rs @@ -29,7 +29,7 @@ impl<'a> Confirmation<'a> { }; loop { - print!("{} [{}Y{}/{}n{}] ", message, colors::green(), colors::reset(), colors::red(), colors::reset()); + print!("{} [{}Y{}/{}n{}] ", message, *colors::GREEN, *colors::RESET, *colors::RED, *colors::RESET); io::stdout().flush()?; let mut answer = String::new(); diff --git a/src/error.rs b/src/error.rs index caf7e6c..9a7e673 100644 --- a/src/error.rs +++ b/src/error.rs @@ -45,11 +45,11 @@ pub struct FinalError { impl Display for FinalError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // Title - writeln!(f, "{}[ERROR]{} {}", red(), reset(), self.title)?; + writeln!(f, "{}[ERROR]{} {}", *RED, *RESET, self.title)?; // Details for detail in &self.details { - writeln!(f, " {}-{} {}", white(), yellow(), detail)?; + writeln!(f, " {}-{} {}", *WHITE, *YELLOW, detail)?; } // Hints @@ -57,11 +57,11 @@ impl Display for FinalError { // Separate by one blank line. writeln!(f)?; for hint in &self.hints { - writeln!(f, "{}hint:{} {}", green(), reset(), hint)?; + writeln!(f, "{}hint:{} {}", *GREEN, *RESET, hint)?; } } - write!(f, "{}", reset()) + write!(f, "{}", *RESET) } } @@ -139,7 +139,7 @@ impl fmt::Display for Error { .detail("This should not have happened") .detail("It's probably our fault") .detail("Please help us improve by reporting the issue at:") - .detail(format!(" {}https://github.com/vrmiguel/ouch/issues ", cyan())) + .detail(format!(" {}https://github.com/vrmiguel/ouch/issues ", *CYAN)) .clone(); error @@ -147,7 +147,7 @@ impl fmt::Display for Error { Error::OofError(err) => FinalError::with_title(err), Error::IoError { reason } => FinalError::with_title(reason), Error::CompressionTypo => FinalError::with_title("Possible typo detected") - .hint(format!("Did you mean '{}ouch compress{}'?", magenta(), reset())) + .hint(format!("Did you mean '{}ouch compress{}'?", *MAGENTA, *RESET)) .clone(), Error::UnknownExtensionError(_) => todo!(), Error::AlreadyExists => todo!(), diff --git a/src/lib.rs b/src/lib.rs index 52537a8..3c3830d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,21 +19,11 @@ mod utils; pub use error::{Error, Result}; -use lazy_static::lazy_static; - /// The status code ouch has when an error is encountered pub const EXIT_FAILURE: i32 = libc::EXIT_FAILURE; const VERSION: &str = env!("CARGO_PKG_VERSION"); -lazy_static! { - static ref NO_COLOR_IS_SET: bool = { - use std::env; - - env::var("NO_COLOR").is_ok() || atty::isnt(atty::Stream::Stdout) || atty::isnt(atty::Stream::Stderr) - }; -} - fn help_command() { use utils::colors::*; @@ -62,17 +52,17 @@ fn help_command() { another folder. Visit https://github.com/ouch-org/ouch for more usage examples.", - magenta = magenta(), - white = white(), - green = green(), - yellow = yellow(), - reset = reset(), - cyan = cyan() + magenta = *MAGENTA, + white = *WHITE, + green = *GREEN, + yellow = *YELLOW, + reset = *RESET, + cyan = *CYAN ); } #[inline] fn version_command() { use utils::colors::*; - println!("{green}ouch{reset} {}", crate::VERSION, green = green(), reset = reset()); + println!("{green}ouch{reset} {}", crate::VERSION, green = *GREEN, reset = *RESET); } diff --git a/src/macros.rs b/src/macros.rs index bed8ba9..14996e3 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -1,5 +1,3 @@ -use crate::NO_COLOR_IS_SET; - #[macro_export] macro_rules! info { ($writer:expr, $($arg:tt)*) => { @@ -14,11 +12,7 @@ macro_rules! info { } pub fn _info_helper() { - use crate::utils::colors::{reset, yellow}; + use crate::utils::colors::{RESET, YELLOW}; - if *NO_COLOR_IS_SET { - print!("[INFO] "); - } else { - print!("{}[INFO]{} ", yellow(), reset()); - } + print!("{}[INFO]{} ", *YELLOW, *RESET); } diff --git a/src/utils.rs b/src/utils.rs index a03d227..74b7210 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -64,52 +64,31 @@ pub struct Bytes { /// Module with a list of bright colors. #[allow(dead_code)] -#[cfg(target_family = "unix")] pub mod colors { - pub const fn reset() -> &'static str { - "\u{1b}[39m" + use once_cell::sync::Lazy; + + static NO_COLOR_IS_SET: Lazy = Lazy::new(|| { + std::env::var_os("NO_COLOR").is_some() || atty::isnt(atty::Stream::Stdout) || atty::isnt(atty::Stream::Stderr) + }); + + macro_rules! color { + ($name:ident = $value:literal) => { + #[cfg(target_family = "unix")] + pub static $name: Lazy<&str> = Lazy::new(|| if *NO_COLOR_IS_SET { "" } else { $value }); + #[cfg(not(target_family = "unix"))] + pub static $name: &&str = &""; + }; } - pub const fn black() -> &'static str { - "\u{1b}[38;5;8m" - } - pub const fn blue() -> &'static str { - "\u{1b}[38;5;12m" - } - pub const fn cyan() -> &'static str { - "\u{1b}[38;5;14m" - } - pub const fn green() -> &'static str { - "\u{1b}[38;5;10m" - } - pub const fn magenta() -> &'static str { - "\u{1b}[38;5;13m" - } - pub const fn red() -> &'static str { - "\u{1b}[38;5;9m" - } - pub const fn white() -> &'static str { - "\u{1b}[38;5;15m" - } - pub const fn yellow() -> &'static str { - "\u{1b}[38;5;11m" - } -} -// Windows does not support ANSI escape codes -#[allow(dead_code, non_upper_case_globals)] -#[cfg(not(target_family = "unix"))] -pub mod colors { - pub const fn empty() -> &'static str { - "" - } - pub const reset: fn() -> &'static str = empty; - pub const black: fn() -> &'static str = empty; - pub const blue: fn() -> &'static str = empty; - pub const cyan: fn() -> &'static str = empty; - pub const green: fn() -> &'static str = empty; - pub const magenta: fn() -> &'static str = empty; - pub const red: fn() -> &'static str = empty; - pub const white: fn() -> &'static str = empty; - pub const yellow: fn() -> &'static str = empty; + + color!(RESET = "\u{1b}[39m"); + color!(BLACK = "\u{1b}[38;5;8m"); + color!(BLUE = "\u{1b}[38;5;12m"); + color!(CYAN = "\u{1b}[38;5;14m"); + color!(GREEN = "\u{1b}[38;5;10m"); + color!(MAGENTA = "\u{1b}[38;5;13m"); + color!(RED = "\u{1b}[38;5;9m"); + color!(WHITE = "\u{1b}[38;5;15m"); + color!(YELLOW = "\u{1b}[38;5;11m"); } impl Bytes { From 05f82d3aee2659254d99af7869ec85dc0210d3e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=2E=20Bezerra?= Date: Fri, 15 Oct 2021 05:32:44 -0300 Subject: [PATCH 12/36] Adding unwrap safety for file_name --- src/commands.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/commands.rs b/src/commands.rs index 32c60fc..fa90b01 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -114,6 +114,11 @@ pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> { && input_extensions.len() < formats.len() && input_extensions.iter().zip(&formats).all(|(inp, out)| inp == out) { + // Safety: + // We checked above that input_extensions isn't empty, so files[0] has a extension. + // + // Path::extension says: "if there is no file_name, then there is no extension". + // Using DeMorgan's law: "if there is extension, then there is file_name". info!( "Partial compression detected. Compressing {} into {}", to_utf(files[0].as_path().file_name().unwrap()), From d852a5897cc111c9f0ec9dd5f384bc9fce1d66cc Mon Sep 17 00:00:00 2001 From: Spyros Roum Date: Fri, 15 Oct 2021 14:20:49 +0300 Subject: [PATCH 13/36] Change FinalError builder pattern to take and give ownership of self This means that when you do `let e = FinalError::with_title("Foo").detail("Blah");`, `e` will be of type `FinalError` instead of `&mut FinalError`, thus you don't have to call `clone()` on it --- src/commands.rs | 9 +++------ src/error.rs | 38 ++++++++++---------------------------- 2 files changed, 13 insertions(+), 34 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 05db7a6..d41ca83 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -50,8 +50,7 @@ pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> { .hint("") .hint("Examples:") .hint(format!(" ouch compress ... {}.tar.gz", to_utf(&output_path))) - .hint(format!(" ouch compress ... {}.zip", to_utf(&output_path))) - .clone(); + .hint(format!(" ouch compress ... {}.zip", to_utf(&output_path))); return Err(Error::with_reason(reason)); } @@ -79,8 +78,7 @@ pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> { .detail("The only supported formats that archive files into an archive are .tar and .zip.") .hint(format!("Try inserting '.tar' or '.zip' before '{}'.", &formats[0])) .hint(format!("From: {}", output_path)) - .hint(format!(" To : {}", suggested_output_path)) - .clone(); + .hint(format!(" To : {}", suggested_output_path)); return Err(Error::with_reason(reason)); } @@ -90,8 +88,7 @@ pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> { .detail(format!("Found the format '{}' in an incorrect position.", format)) .detail(format!("'{}' can only be used at the start of the file extension.", format)) .hint(format!("If you wish to compress multiple files, start the extension with '{}'.", format)) - .hint(format!("Otherwise, remove the last '{}' from '{}'.", format, to_utf(&output_path))) - .clone(); + .hint(format!("Otherwise, remove the last '{}' from '{}'.", format, to_utf(&output_path))); return Err(Error::with_reason(reason)); } diff --git a/src/error.rs b/src/error.rs index caf7e6c..b0e313e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -70,12 +70,12 @@ impl FinalError { Self { title: title.to_string(), details: vec![], hints: vec![] } } - pub fn detail(&mut self, detail: impl ToString) -> &mut Self { + pub fn detail(mut self, detail: impl ToString) -> Self { self.details.push(detail.to_string()); self } - pub fn hint(&mut self, hint: impl ToString) -> &mut Self { + pub fn hint(mut self, hint: impl ToString) -> Self { self.hints.push(hint.to_string()); self } @@ -85,70 +85,52 @@ impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let err = match self { Error::MissingExtensionError(filename) => { - let error = FinalError::with_title(format!("Cannot compress to {:?}", filename)) + FinalError::with_title(format!("Cannot compress to {:?}", filename)) .detail("Ouch could not detect the compression format") .hint("Use a supported format extension, like '.zip' or '.tar.gz'") .hint("Check https://github.com/vrmiguel/ouch for a full list of supported formats") - .clone(); - - error } Error::WalkdirError { reason } => FinalError::with_title(reason), Error::FileNotFound(file) => { - let error = if file == Path::new("") { + if file == Path::new("") { FinalError::with_title("file not found!") } else { FinalError::with_title(format!("file {:?} not found!", file)) - }; - - error + } } Error::CompressingRootFolder => { - let error = FinalError::with_title("It seems you're trying to compress the root folder.") + FinalError::with_title("It seems you're trying to compress the root folder.") .detail("This is unadvisable since ouch does compressions in-memory.") .hint("Use a more appropriate tool for this, such as rsync.") - .clone(); - - error } Error::MissingArgumentsForCompression => { - let error = FinalError::with_title("Could not compress") + FinalError::with_title("Could not compress") .detail("The compress command requires at least 2 arguments") .hint("You must provide:") .hint(" - At least one input argument.") .hint(" - The output argument.") .hint("") .hint("Example: `ouch compress image.png img.zip`") - .clone(); - - error } Error::MissingArgumentsForDecompression => { - let error = FinalError::with_title("Could not decompress") + FinalError::with_title("Could not decompress") .detail("The compress command requires at least one argument") .hint("You must provide:") .hint(" - At least one input argument.") .hint("") .hint("Example: `ouch decompress imgs.tar.gz`") - .clone(); - - error } Error::InternalError => { - let error = FinalError::with_title("InternalError :(") + FinalError::with_title("InternalError :(") .detail("This should not have happened") .detail("It's probably our fault") .detail("Please help us improve by reporting the issue at:") .detail(format!(" {}https://github.com/vrmiguel/ouch/issues ", cyan())) - .clone(); - - error } Error::OofError(err) => FinalError::with_title(err), Error::IoError { reason } => FinalError::with_title(reason), Error::CompressionTypo => FinalError::with_title("Possible typo detected") - .hint(format!("Did you mean '{}ouch compress{}'?", magenta(), reset())) - .clone(), + .hint(format!("Did you mean '{}ouch compress{}'?", magenta(), reset())), Error::UnknownExtensionError(_) => todo!(), Error::AlreadyExists => todo!(), Error::InvalidZipArchive(_) => todo!(), From 702e7622db61a809c9b21a501a66f904ce705558 Mon Sep 17 00:00:00 2001 From: Spyros Roum Date: Fri, 15 Oct 2021 14:49:05 +0300 Subject: [PATCH 14/36] Run `cargo fmt` removing redundant braces --- src/error.rs | 49 +++++++++++++++++++++++-------------------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/src/error.rs b/src/error.rs index b0e313e..62a6b55 100644 --- a/src/error.rs +++ b/src/error.rs @@ -103,34 +103,31 @@ impl fmt::Display for Error { .detail("This is unadvisable since ouch does compressions in-memory.") .hint("Use a more appropriate tool for this, such as rsync.") } - Error::MissingArgumentsForCompression => { - FinalError::with_title("Could not compress") - .detail("The compress command requires at least 2 arguments") - .hint("You must provide:") - .hint(" - At least one input argument.") - .hint(" - The output argument.") - .hint("") - .hint("Example: `ouch compress image.png img.zip`") - } - Error::MissingArgumentsForDecompression => { - FinalError::with_title("Could not decompress") - .detail("The compress command requires at least one argument") - .hint("You must provide:") - .hint(" - At least one input argument.") - .hint("") - .hint("Example: `ouch decompress imgs.tar.gz`") - } - Error::InternalError => { - FinalError::with_title("InternalError :(") - .detail("This should not have happened") - .detail("It's probably our fault") - .detail("Please help us improve by reporting the issue at:") - .detail(format!(" {}https://github.com/vrmiguel/ouch/issues ", cyan())) - } + Error::MissingArgumentsForCompression => FinalError::with_title("Could not compress") + .detail("The compress command requires at least 2 arguments") + .hint("You must provide:") + .hint(" - At least one input argument.") + .hint(" - The output argument.") + .hint("") + .hint("Example: `ouch compress image.png img.zip`"), + Error::MissingArgumentsForDecompression => FinalError::with_title("Could not decompress") + .detail("The compress command requires at least one argument") + .hint("You must provide:") + .hint(" - At least one input argument.") + .hint("") + .hint("Example: `ouch decompress imgs.tar.gz`"), + Error::InternalError => FinalError::with_title("InternalError :(") + .detail("This should not have happened") + .detail("It's probably our fault") + .detail("Please help us improve by reporting the issue at:") + .detail(format!(" {}https://github.com/vrmiguel/ouch/issues ", cyan())), Error::OofError(err) => FinalError::with_title(err), Error::IoError { reason } => FinalError::with_title(reason), - Error::CompressionTypo => FinalError::with_title("Possible typo detected") - .hint(format!("Did you mean '{}ouch compress{}'?", magenta(), reset())), + Error::CompressionTypo => FinalError::with_title("Possible typo detected").hint(format!( + "Did you mean '{}ouch compress{}'?", + magenta(), + reset() + )), Error::UnknownExtensionError(_) => todo!(), Error::AlreadyExists => todo!(), Error::InvalidZipArchive(_) => todo!(), From 15c54a615de74ec32ae3cabf26394ac4222faa4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=2E=20Bezerra?= Date: Fri, 15 Oct 2021 09:35:07 -0300 Subject: [PATCH 15/36] Renaming NO_COLOR_IS_SET to DISABLE_COLORED_TEXT --- src/utils.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index 74b7210..a372051 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -67,14 +67,14 @@ pub struct Bytes { pub mod colors { use once_cell::sync::Lazy; - static NO_COLOR_IS_SET: Lazy = Lazy::new(|| { + static DISABLE_COLORED_TEXT: Lazy = Lazy::new(|| { std::env::var_os("NO_COLOR").is_some() || atty::isnt(atty::Stream::Stdout) || atty::isnt(atty::Stream::Stderr) }); macro_rules! color { ($name:ident = $value:literal) => { #[cfg(target_family = "unix")] - pub static $name: Lazy<&str> = Lazy::new(|| if *NO_COLOR_IS_SET { "" } else { $value }); + pub static $name: Lazy<&str> = Lazy::new(|| if *DISABLE_COLORED_TEXT { "" } else { $value }); #[cfg(not(target_family = "unix"))] pub static $name: &&str = &""; }; From cf7b3c6e1f1d7324c8f4891d5d208ea66f35eaaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Marcos=20Bezerra?= Date: Fri, 15 Oct 2021 10:47:37 -0300 Subject: [PATCH 16/36] Create CONTRIBUTING.md --- CONTRIBUTING.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..67be18c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,15 @@ +Thanks for your interest in contributing to `ouch`! + +Feel free to open an issue anytime you wish to ask a question, suggest a feature, report a bug, etc. + +# Requirements + +1. Be kind, considerate and respectfull. +2. If editing .rs files, run `rustfmt` on them before commiting. + +Note that we are using `unstable` features of `rustfmt`, so you will need to change your toolchain to nightly. + +# Suggestions + +1. Ask for some guidance before solving an error if you feel like it. +2. If editing Rust code, run `clippy` before commiting. From c44cdf10130d491e2277c27e6dc1c6f2980f94bd Mon Sep 17 00:00:00 2001 From: Spyros Roum Date: Fri, 15 Oct 2021 17:11:44 +0300 Subject: [PATCH 17/36] Enable nightly for unstable fmt feature and run `cargo fmt` --- rust-toolchain | 1 + rustfmt.toml | 2 ++ src/error.rs | 54 +++++++++++++++++++++++++++--------------------- src/oof/error.rs | 24 ++++++++++++--------- 4 files changed, 48 insertions(+), 33 deletions(-) create mode 100644 rust-toolchain diff --git a/rust-toolchain b/rust-toolchain new file mode 100644 index 0000000..bf867e0 --- /dev/null +++ b/rust-toolchain @@ -0,0 +1 @@ +nightly diff --git a/rustfmt.toml b/rustfmt.toml index 7bba11e..2ac63bb 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -6,3 +6,5 @@ reorder_imports = true reorder_modules = true use_try_shorthand = true use_small_heuristics = "Max" +unstable_features = true +force_multiline_blocks = true diff --git a/src/error.rs b/src/error.rs index 62a6b55..571507e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -103,31 +103,39 @@ impl fmt::Display for Error { .detail("This is unadvisable since ouch does compressions in-memory.") .hint("Use a more appropriate tool for this, such as rsync.") } - Error::MissingArgumentsForCompression => FinalError::with_title("Could not compress") - .detail("The compress command requires at least 2 arguments") - .hint("You must provide:") - .hint(" - At least one input argument.") - .hint(" - The output argument.") - .hint("") - .hint("Example: `ouch compress image.png img.zip`"), - Error::MissingArgumentsForDecompression => FinalError::with_title("Could not decompress") - .detail("The compress command requires at least one argument") - .hint("You must provide:") - .hint(" - At least one input argument.") - .hint("") - .hint("Example: `ouch decompress imgs.tar.gz`"), - Error::InternalError => FinalError::with_title("InternalError :(") - .detail("This should not have happened") - .detail("It's probably our fault") - .detail("Please help us improve by reporting the issue at:") - .detail(format!(" {}https://github.com/vrmiguel/ouch/issues ", cyan())), + Error::MissingArgumentsForCompression => { + FinalError::with_title("Could not compress") + .detail("The compress command requires at least 2 arguments") + .hint("You must provide:") + .hint(" - At least one input argument.") + .hint(" - The output argument.") + .hint("") + .hint("Example: `ouch compress image.png img.zip`") + } + Error::MissingArgumentsForDecompression => { + FinalError::with_title("Could not decompress") + .detail("The compress command requires at least one argument") + .hint("You must provide:") + .hint(" - At least one input argument.") + .hint("") + .hint("Example: `ouch decompress imgs.tar.gz`") + } + Error::InternalError => { + FinalError::with_title("InternalError :(") + .detail("This should not have happened") + .detail("It's probably our fault") + .detail("Please help us improve by reporting the issue at:") + .detail(format!(" {}https://github.com/vrmiguel/ouch/issues ", cyan())) + } Error::OofError(err) => FinalError::with_title(err), Error::IoError { reason } => FinalError::with_title(reason), - Error::CompressionTypo => FinalError::with_title("Possible typo detected").hint(format!( - "Did you mean '{}ouch compress{}'?", - magenta(), - reset() - )), + Error::CompressionTypo => { + FinalError::with_title("Possible typo detected").hint(format!( + "Did you mean '{}ouch compress{}'?", + magenta(), + reset() + )) + } Error::UnknownExtensionError(_) => todo!(), Error::AlreadyExists => todo!(), Error::InvalidZipArchive(_) => todo!(), diff --git a/src/oof/error.rs b/src/oof/error.rs index 6dd4783..299601a 100644 --- a/src/oof/error.rs +++ b/src/oof/error.rs @@ -31,18 +31,22 @@ impl fmt::Display for OofError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { // TODO: implement proper debug messages match self { - OofError::FlagValueConflict { flag, previous_value, new_value } => write!( - f, - "CLI flag value conflicted for flag '--{}', previous: {:?}, new: {:?}.", - flag.long, previous_value, new_value - ), + OofError::FlagValueConflict { flag, previous_value, new_value } => { + write!( + f, + "CLI flag value conflicted for flag '--{}', previous: {:?}, new: {:?}.", + flag.long, previous_value, new_value + ) + } OofError::InvalidUnicode(flag) => write!(f, "{:?} is not valid Unicode.", flag), OofError::UnknownShortFlag(ch) => write!(f, "Unknown argument '-{}'", ch), - OofError::MisplacedShortArgFlagError(ch) => write!( - f, - "Invalid placement of `-{}`.\nOnly the last letter in a sequence of short flags can take values.", - ch - ), + OofError::MisplacedShortArgFlagError(ch) => { + write!( + f, + "Invalid placement of `-{}`.\nOnly the last letter in a sequence of short flags can take values.", + ch + ) + } OofError::MissingValueToFlag(flag) => write!(f, "Flag {} takes value but none was supplied.", flag), OofError::DuplicatedFlag(flag) => write!(f, "Duplicated usage of {}.", flag), OofError::UnknownLongFlag(flag) => write!(f, "Unknown argument '--{}'", flag), From 16ee5139062a8824d74678ba5d1a5fd174811aa7 Mon Sep 17 00:00:00 2001 From: Spyros Roum Date: Fri, 15 Oct 2021 17:21:35 +0300 Subject: [PATCH 18/36] Replace `todo!` with an actual error --- src/cli.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 9ff64c6..d507775 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -11,7 +11,7 @@ use std::{ use strsim::normalized_damerau_levenshtein; -use crate::{arg_flag, flag, oof, Error}; +use crate::{arg_flag, error::FinalError, flag, oof, Error}; #[derive(PartialEq, Eq, Debug)] pub enum Command { @@ -47,7 +47,13 @@ pub fn parse_args() -> crate::Result { } if parsed_args.flags.is_present("yes") && parsed_args.flags.is_present("no") { - todo!("conflicting flags, better error message."); + return Err(Error::Custom { + reason: FinalError::with_title("Conflicted flags detected.") + .detail("You can't use both --yes and --no at the same time.") + .hint("Use --yes if you want to positively skip overwrite questions") + .hint("Use --no if you want to negatively skip overwrite questions") + .hint("Don't use either if you want to be asked each time"), + }); } Ok(parsed_args) From 1acf6e4d35a0a8f9035f285f7b9acae3afa313dd Mon Sep 17 00:00:00 2001 From: figsoda Date: Sat, 16 Oct 2021 10:10:48 -0400 Subject: [PATCH 19/36] tests: apply clippy lints --- tests/compress_and_decompress.rs | 4 ++-- tests/compress_empty_dir.rs | 4 ++-- tests/utils.rs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/compress_and_decompress.rs b/tests/compress_and_decompress.rs index e97ab33..6e6649f 100644 --- a/tests/compress_and_decompress.rs +++ b/tests/compress_and_decompress.rs @@ -103,9 +103,9 @@ fn test_compressing_and_decompressing_archive(format: &str) { (0..quantity_of_files).map(|_| generate_random_file_content(&mut rng)).collect(); // Create them - let mut file_paths = create_files(&testing_dir_path, &contents_of_files); + let mut file_paths = create_files(testing_dir_path, &contents_of_files); // Compress them - let compressed_archive_path = compress_files(&testing_dir_path, &file_paths, &format); + let compressed_archive_path = compress_files(testing_dir_path, &file_paths, format); // Decompress them let mut extracted_paths = extract_files(&compressed_archive_path); diff --git a/tests/compress_empty_dir.rs b/tests/compress_empty_dir.rs index 95f3348..57bfe35 100644 --- a/tests/compress_empty_dir.rs +++ b/tests/compress_empty_dir.rs @@ -20,11 +20,11 @@ fn test_compress_decompress_with_empty_dir(format: &str) { let testing_dir_path = testing_dir.path(); - let empty_dir_path: PathBuf = create_empty_dir(&testing_dir_path, "dummy_empty_dir_name"); + let empty_dir_path: PathBuf = create_empty_dir(testing_dir_path, "dummy_empty_dir_name"); let mut file_paths: Vec = vec![empty_dir_path]; - let compressed_archive_path: PathBuf = compress_files(&testing_dir_path, &file_paths, &format); + let compressed_archive_path: PathBuf = compress_files(testing_dir_path, &file_paths, format); let mut extracted_paths = extract_files(&compressed_archive_path); diff --git a/tests/utils.rs b/tests/utils.rs index 3ab9831..05b4b0f 100644 --- a/tests/utils.rs +++ b/tests/utils.rs @@ -22,7 +22,7 @@ pub fn compress_files(at: &Path, paths_to_compress: &[PathBuf], format: &str) -> let archive_path = String::from("archive.") + format; let archive_path = at.join(archive_path); - let command = Command::Compress { files: paths_to_compress.to_vec(), output_path: archive_path.to_path_buf() }; + let command = Command::Compress { files: paths_to_compress.to_vec(), output_path: archive_path.clone() }; run(command, &oof::Flags::default()).expect("Failed to compress test dummy files"); archive_path From 06af320595a29a9876921962f5b77dabcec8b074 Mon Sep 17 00:00:00 2001 From: Dominik Nakamura Date: Mon, 18 Oct 2021 00:42:35 +0900 Subject: [PATCH 20/36] Add support for short tar archive extensions --- src/commands.rs | 37 ++++++++++++++++++++++++++++---- src/extension.rs | 23 ++++++++++++++------ tests/compress_and_decompress.rs | 17 ++++++++++++++- 3 files changed, 65 insertions(+), 12 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index ab51008..de2722d 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -246,10 +246,24 @@ fn compress_files( writer.flush()?; } Tgz => { - // Wrap it into an gz_decoder, and pass to the tar archive builder - let gz_decoder = flate2::write::GzEncoder::new(writer, Default::default()); - let mut writer = archive::tar::build_archive_from_paths(&files, gz_decoder)?; - writer.flush()?; + let encoder = flate2::write::GzEncoder::new(writer, Default::default()); + let writer = archive::tar::build_archive_from_paths(&files, encoder)?; + writer.finish()?.flush()?; + } + Tbz => { + let encoder = bzip2::write::BzEncoder::new(writer, Default::default()); + let writer = archive::tar::build_archive_from_paths(&files, encoder)?; + writer.finish()?.flush()?; + } + Tlzma => { + let encoder = xz2::write::XzEncoder::new(writer, 6); + let writer = archive::tar::build_archive_from_paths(&files, encoder)?; + writer.finish()?.flush()?; + } + Tzst => { + let encoder = zstd::stream::write::Encoder::new(writer, Default::default())?; + let writer = archive::tar::build_archive_from_paths(&files, encoder)?; + writer.finish()?.flush()?; } Zip => { eprintln!("{yellow}Warning:{reset}", yellow = *colors::YELLOW, reset = *colors::RESET); @@ -349,6 +363,21 @@ fn decompress_file( let _ = crate::archive::tar::unpack_archive(reader, output_folder, flags)?; info!("Successfully uncompressed archive in '{}'.", to_utf(output_folder)); } + Tbz => { + let reader = chain_reader_decoder(&Bzip, reader)?; + let _ = crate::archive::tar::unpack_archive(reader, output_folder, flags)?; + info!("Successfully uncompressed archive in '{}'.", to_utf(output_folder)); + } + Tlzma => { + let reader = chain_reader_decoder(&Lzma, reader)?; + let _ = crate::archive::tar::unpack_archive(reader, output_folder, flags)?; + info!("Successfully uncompressed archive in '{}'.", to_utf(output_folder)); + } + Tzst => { + let reader = chain_reader_decoder(&Zstd, reader)?; + let _ = crate::archive::tar::unpack_archive(reader, output_folder, flags)?; + info!("Successfully uncompressed archive in '{}'.", to_utf(output_folder)); + } Zip => { eprintln!("Compressing first into .zip."); eprintln!("Warning: .zip archives with extra extensions have a downside."); diff --git a/src/extension.rs b/src/extension.rs index aa2777a..b1828f0 100644 --- a/src/extension.rs +++ b/src/extension.rs @@ -7,13 +7,16 @@ use self::CompressionFormat::*; #[derive(Clone, PartialEq, Eq, Debug)] /// Accepted extensions for input and output pub enum CompressionFormat { - Gzip, // .gz - Bzip, // .bz - Lzma, // .lzma - Tar, // .tar (technically not a compression extension, but will do for now) - Tgz, // .tgz - Zstd, // .zst - Zip, // .zip + Gzip, // .gz + Bzip, // .bz + Lzma, // .lzma + Tar, // .tar (technically not a compression extension, but will do for now) + Tgz, // .tgz + Tbz, // .tbz + Tlzma, // .tlzma + Tzst, // .tzst + Zstd, // .zst + Zip, // .zip } impl fmt::Display for CompressionFormat { @@ -28,6 +31,9 @@ impl fmt::Display for CompressionFormat { Lzma => ".lz", Tar => ".tar", Tgz => ".tgz", + Tbz => ".tbz", + Tlzma => ".tlz", + Tzst => ".tzst", Zip => ".zip", } ) @@ -50,6 +56,9 @@ pub fn separate_known_extensions_from_name(mut path: &Path) -> (&Path, Vec Tar, "tgz" => Tgz, + "tbz" | "tbz2" => Tbz, + "txz" | "tlz" | "tlzma" => Tlzma, + "tzst" => Tzst, "zip" => Zip, "bz" | "bz2" => Bzip, "gz" => Gzip, diff --git a/tests/compress_and_decompress.rs b/tests/compress_and_decompress.rs index e97ab33..08e5367 100644 --- a/tests/compress_and_decompress.rs +++ b/tests/compress_and_decompress.rs @@ -27,7 +27,10 @@ fn sanity_check_through_mime() { let bytes = generate_random_file_content(&mut SmallRng::from_entropy()); test_file.write_all(&bytes).expect("to successfully write bytes to the file"); - let formats = ["tar", "zip", "tar.gz", "tgz", "tar.bz", "tar.bz2", "tar.lzma", "tar.xz", "tar.zst"]; + let formats = [ + "tar", "zip", "tar.gz", "tgz", "tbz", "tbz2", "txz", "tlz", "tlzma", "tzst", "tar.bz", "tar.bz2", "tar.lzma", + "tar.xz", "tar.zst", + ]; let expected_mimes = [ "application/x-tar", @@ -38,6 +41,12 @@ fn sanity_check_through_mime() { "application/x-bzip2", "application/x-xz", "application/x-xz", + "application/x-xz", + "application/zstd", + "application/x-bzip2", + "application/x-bzip2", + "application/x-xz", + "application/x-xz", "application/zstd", ]; @@ -69,6 +78,12 @@ fn test_each_format() { test_compressing_and_decompressing_archive("tar.lzma"); test_compressing_and_decompressing_archive("tar.zst"); test_compressing_and_decompressing_archive("tgz"); + test_compressing_and_decompressing_archive("tbz"); + test_compressing_and_decompressing_archive("tbz2"); + test_compressing_and_decompressing_archive("txz"); + test_compressing_and_decompressing_archive("tlz"); + test_compressing_and_decompressing_archive("tlzma"); + test_compressing_and_decompressing_archive("tzst"); test_compressing_and_decompressing_archive("zip"); test_compressing_and_decompressing_archive("zip.gz"); test_compressing_and_decompressing_archive("zip.bz"); From 2feefb3ca35b210e41c3f132210ce01cfebc38e6 Mon Sep 17 00:00:00 2001 From: figsoda Date: Sun, 17 Oct 2021 17:54:40 -0400 Subject: [PATCH 21/36] minor cleanups --- src/macros.rs | 11 +++-------- src/oof/util.rs | 19 +------------------ src/utils.rs | 9 ++------- 3 files changed, 6 insertions(+), 33 deletions(-) diff --git a/src/macros.rs b/src/macros.rs index 14996e3..76877fa 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -1,13 +1,8 @@ #[macro_export] macro_rules! info { - ($writer:expr, $($arg:tt)*) => { - use crate::macros::_info_helper; - _info_helper(); - println!($writer, $($arg)*); - }; - ($writer:expr) => { - _info_helper(); - println!($writer); + ($($arg:tt)*) => { + $crate::macros::_info_helper(); + println!($($arg)*); }; } diff --git a/src/oof/util.rs b/src/oof/util.rs index 5479818..0e75350 100644 --- a/src/oof/util.rs +++ b/src/oof/util.rs @@ -1,23 +1,11 @@ /// Util function to skip the two leading long flag hyphens. pub fn trim_double_hyphen(flag_text: &str) -> &str { - let mut chars = flag_text.chars(); - chars.nth(1); // Skipping 2 chars - chars.as_str() -} - -// Currently unused -/// Util function to skip the single leading short flag hyphen. -pub fn trim_single_hyphen(flag_text: &str) -> &str { - let mut chars = flag_text.chars(); - - chars.next(); // Skipping 1 char - chars.as_str() + flag_text.get(2..).unwrap_or_default() } #[cfg(test)] mod tests { use super::trim_double_hyphen; - use super::trim_single_hyphen; #[test] fn _trim_double_hyphen() { @@ -25,9 +13,4 @@ mod tests { assert_eq!(trim_double_hyphen("--verbose"), "verbose"); assert_eq!(trim_double_hyphen("--help"), "help"); } - - fn _trim_single_hyphen() { - assert_eq!(trim_single_hyphen("-vv"), "vv"); - assert_eq!(trim_single_hyphen("-h"), "h"); - } } diff --git a/src/utils.rs b/src/utils.rs index a372051..71e0337 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -11,7 +11,7 @@ use crate::{dialogs::Confirmation, info, oof}; pub fn dir_is_empty(dir_path: &Path) -> bool { let is_empty = |mut rd: ReadDir| rd.next().is_none(); - dir_path.read_dir().ok().map(is_empty).unwrap_or_default() + dir_path.read_dir().map(is_empty).unwrap_or_default() } pub fn create_dir_if_non_existent(path: &Path) -> crate::Result<()> { @@ -45,12 +45,7 @@ pub fn user_wants_to_overwrite(path: &Path, flags: &oof::Flags) -> crate::Result _ => {} } - let file_path_str = to_utf(path); - - const OVERWRITE_CONFIRMATION_QUESTION: Confirmation = - Confirmation::new("Do you want to overwrite 'FILE'?", Some("FILE")); - - OVERWRITE_CONFIRMATION_QUESTION.ask(Some(&file_path_str)) + Confirmation::new("Do you want to overwrite 'FILE'?", Some("FILE")).ask(Some(&to_utf(path))) } pub fn to_utf(os_str: impl AsRef) -> String { From 765fb40ec79ca02d786583daefc3def23c6773bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Marcos=20Bezerra?= Date: Mon, 18 Oct 2021 23:09:28 -0300 Subject: [PATCH 22/36] Readme revision (#102) + Added list of aliases to the README --- README.md | 109 ++++++++++++++++++++---------------------------------- 1 file changed, 41 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index cea0c4c..da939fe 100644 --- a/README.md +++ b/README.md @@ -2,124 +2,97 @@ [![crates.io](https://img.shields.io/crates/v/ouch.svg?style=for-the-badge&logo=rust)](https://crates.io/crates/ouch) [![license](https://img.shields.io/badge/license-MIT-blue.svg?style=for-the-badge&logo=Open-Source-Initiative&logoColor=ffffff)](https://github.com/ouch-org/ouch/blob/main/LICENSE) - - -`ouch` stands for **Obvious Unified Compression Helper**, and works on _Linux_, _Mac OS_ and _Windows_. - -It is a CLI tool to compress and decompress files that aims on ease of usage. - - - +`ouch` stands for **Obvious Unified Compression Helper**, it's a CLI tool to compress and decompress files. +- [Features](#features) - [Usage](#usage) - - [Decompressing](#decompressing) - - [Compressing](#compressing) - [Installation](#installation) - - [Latest binary](#downloading-the-latest-binary) - - [Compiling from source](#installing-from-source-code) - [Supported Formats](#supported-formats) - [Contributing](#contributing) +## Features + +1. Easy to use. +2. Automatic format detection. +3. Same syntax, various formats. +4. Encoding and decoding streams, it's fast. + ## Usage ### Decompressing -Run `ouch` and pass compressed files as arguments. +Use the `decompress` subcommand and pass the files. ```sh -# Decompress 'a.zip' +# Decompress one ouch decompress a.zip -# Also works with the short version -ouch d a.zip +# Decompress multiple +ouch decompress a.zip b.tar.gz c.tar -# Decompress multiple files -ouch decompress a.zip b.tar.gz +# Short alternative +ouch d a.zip ``` -You can redirect the decompression results to a folder with the `-o/--output` flag. +You can redirect the decompression results to another folder with the `-o/--output` flag. ```sh -# Create 'pictures' folder and decompress inside of it -ouch decompress a.zip -o pictures +# Decompress 'summer_vacation.zip' inside of new folder 'pictures' +ouch decompress summer_vacation.zip -o pictures ``` ### Compressing -Use the `compress` subcommand. - -Accepts multiple files and folders, the **last** argument shall be the **output file**. +Use the `compress` subcommand, pass the files and the **output file** at the end. ```sh -# Compress four files into 'archive.zip' +# Compress four files/folders ouch compress 1 2 3 4 archive.zip -# Also works with the short version -ouch c 1 2 3 4 archive.zip +# Short alternative +ouch c file.txt file.zip -# Compress folder and video into 'videos.tar.gz' -ouch compress videos/ meme.mp4 videos.tar.gz - -# Compress one file using 4 compression formats -ouch compress file.txt compressed.gz.xz.bz.zst - -# Compress all the files in current folder -ouch compress * files.zip +# Compress everything in the current folder again and again +ouch compress * everything.tar.gz.xz.bz.zst.gz.gz.gz.gz.gz ``` `ouch` checks for the extensions of the **output file** to decide which formats should be used. - - ## Installation ### Downloading the latest binary -Download the script with `curl` and run it. +Compiled for `x86_64` on _Linux_, _Mac OS_ and _Windows_, run with `curl` or `wget`. -```sh -curl -s https://raw.githubusercontent.com/ouch-org/ouch/master/install.sh | sh -``` +| Method | Command | +|:---------:|:-----------------------------------------------------------------------------------| +| **curl** | `curl -s https://raw.githubusercontent.com/ouch-org/ouch/master/install.sh | sh` | +| **wget** | `wget https://raw.githubusercontent.com/ouch-org/ouch/master/install.sh -O - | sh` | -Or with `wget`. -```sh -wget https://raw.githubusercontent.com/ouch-org/ouch/master/install.sh -O - | sh -``` - -The script will download the latest binary and copy it to `/usr/bin`. +The script will download the [latest binary](https://github.com/ouch-org/ouch/releases) and copy it to `/usr/bin`. ### Installing from source code -For compiling, check [the wiki guide](https://github.com/ouch-org/ouch/wiki/Compiling-and-installing-from-source-code). - +For compiling, check the [wiki guide](https://github.com/ouch-org/ouch/wiki/Compiling-and-installing-from-source-code). ## Supported formats -| | .tar, .tgz | .zip | .bz, .bz2 | .gz | .xz, .lz, .lzma | .zst | +| Format | .tar | .zip | .bz, .bz2 | .gz | .xz, .lz, .lzma | .zst | |:-------------:|:----:|:----:|:---------:| --- |:---------------:| --- | -| Decompression | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Compression | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Supported | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -Note that formats can be chained: -- `.tar.gz` -- `.tar.xz` -- `.tar.gz.xz` -- `.tar.gz.gz.gz.gz.gz.gz.gz.gz.gz.gz.gz.gz.gz.gz.gz.gz.gz.gz.lz.lz.lz.lz.lz.lz.lz.lz.lz.lz.bz.bz.bz.bz.bz.bz.bz` -- `.gz.xz` -- etc... +And the aliases: `tgz`, `tbz`, `tbz2`, `txz`, `tlz`, `tlzma`, `tzst`. +Formats can be chained (`ouch` keeps it _fast_): +- `.gz.xz.bz.zst` +- `.tar.gz.xz.bz.zst` +- `.tar.gz.gz.gz.gz.xz.xz.xz.xz.bz.bz.bz.bz.zst.zst.zst.zst` +- ## Contributing `ouch` is 100% made out of voluntary work, any small contribution is welcome! - Open an issue. -- Open a pr. -- Share it to a friend. +- Open a pull request. +- Share it to a friend! From c7b7fa40972724ceb0e7607adc32aafd3bc7bd2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Marcos=20Bezerra?= Date: Tue, 19 Oct 2021 02:39:57 -0300 Subject: [PATCH 23/36] Fix README small markdown error --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index da939fe..ba7114c 100644 --- a/README.md +++ b/README.md @@ -85,10 +85,11 @@ For compiling, check the [wiki guide](https://github.com/ouch-org/ouch/wiki/Comp And the aliases: `tgz`, `tbz`, `tbz2`, `txz`, `tlz`, `tlzma`, `tzst`. Formats can be chained (`ouch` keeps it _fast_): + - `.gz.xz.bz.zst` - `.tar.gz.xz.bz.zst` - `.tar.gz.gz.gz.gz.xz.xz.xz.xz.bz.bz.bz.bz.zst.zst.zst.zst` -- + ## Contributing `ouch` is 100% made out of voluntary work, any small contribution is welcome! From 7a13a6410abfedc31755c154ab5e647faa012206 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Marcos=20Bezerra?= Date: Tue, 19 Oct 2021 04:33:53 -0300 Subject: [PATCH 24/36] Escaping pipes in installation commands This fixes monospaced formatting in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ba7114c..cba6af6 100644 --- a/README.md +++ b/README.md @@ -66,8 +66,8 @@ Compiled for `x86_64` on _Linux_, _Mac OS_ and _Windows_, run with `curl` or `wg | Method | Command | |:---------:|:-----------------------------------------------------------------------------------| -| **curl** | `curl -s https://raw.githubusercontent.com/ouch-org/ouch/master/install.sh | sh` | -| **wget** | `wget https://raw.githubusercontent.com/ouch-org/ouch/master/install.sh -O - | sh` | +| **curl** | `curl -s https://raw.githubusercontent.com/ouch-org/ouch/master/install.sh \| sh` | +| **wget** | `wget https://raw.githubusercontent.com/ouch-org/ouch/master/install.sh -O - \| sh` | The script will download the [latest binary](https://github.com/ouch-org/ouch/releases) and copy it to `/usr/bin`. From e24c9ce931c836399556a56805f8c9177dd5bda5 Mon Sep 17 00:00:00 2001 From: Spyros Roum Date: Tue, 19 Oct 2021 14:42:07 +0300 Subject: [PATCH 25/36] Replace oof with clap --- Cargo.lock | 148 +++++++++++++++++- Cargo.toml | 2 +- src/archive/tar.rs | 10 +- src/archive/zip.rs | 10 +- src/cli.rs | 229 +++++++-------------------- src/commands.rs | 43 +++-- src/error.rs | 10 +- src/lib.rs | 46 ------ src/main.rs | 11 +- src/oof/error.rs | 55 ------- src/oof/flags.rs | 109 ------------- src/oof/mod.rs | 383 --------------------------------------------- src/oof/util.rs | 16 -- src/utils.rs | 17 +- 14 files changed, 244 insertions(+), 845 deletions(-) delete mode 100644 src/oof/error.rs delete mode 100644 src/oof/flags.rs delete mode 100644 src/oof/mod.rs delete mode 100644 src/oof/util.rs diff --git a/Cargo.lock b/Cargo.lock index a800ba3..68b82c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -83,6 +83,37 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "3.0.0-beta.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feff3878564edb93745d58cf63e17b63f24142506e7a20c87a5521ed7bfb1d63" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "indexmap", + "lazy_static", + "os_str_bytes", + "strsim", + "termcolor", + "textwrap", + "unicase", +] + +[[package]] +name = "clap_derive" +version = "3.0.0-beta.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b15c6b4f786ffb6192ffe65a36855bc1fc2444bcd0945ae16748dcd6ed7d0d3" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "crc32fast" version = "1.2.1" @@ -128,6 +159,21 @@ dependencies = [ "wasi", ] +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "hermit-abi" version = "0.1.19" @@ -137,6 +183,16 @@ dependencies = [ "libc", ] +[[package]] +name = "indexmap" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +dependencies = [ + "autocfg", + "hashbrown", +] + [[package]] name = "infer" version = "0.5.0" @@ -155,6 +211,12 @@ dependencies = [ "libc", ] +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.103" @@ -183,6 +245,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + [[package]] name = "miniz_oxide" version = "0.4.4" @@ -199,18 +267,27 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" +[[package]] +name = "os_str_bytes" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addaa943333a514159c80c97ff4a93306530d965d27e139188283cd13e06a799" +dependencies = [ + "memchr", +] + [[package]] name = "ouch" version = "0.2.0" dependencies = [ "atty", "bzip2", + "clap", "flate2", "infer", "libc", "once_cell", "rand", - "strsim", "tar", "tempfile", "walkdir", @@ -231,6 +308,30 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3ca011bd0129ff4ae15cd04c4eef202cadf6c51c21e47aba319b4e0501db741" +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.30" @@ -358,6 +459,24 @@ dependencies = [ "winapi", ] +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" +dependencies = [ + "unicode-width", +] + [[package]] name = "thiserror" version = "1.0.30" @@ -378,6 +497,27 @@ dependencies = [ "syn", ] +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-segmentation" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" + +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + [[package]] name = "unicode-xid" version = "0.2.2" @@ -396,6 +536,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + [[package]] name = "walkdir" version = "2.3.2" diff --git a/Cargo.toml b/Cargo.toml index 071672a..c42ac8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,10 +13,10 @@ description = "A command-line utility for easily compressing and decompressing f # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +clap = "=3.0.0-beta.5" # Keep it pinned while in beta! atty = "0.2.14" once_cell = "1.8.0" walkdir = "2.3.2" -strsim = "0.10.0" bzip2 = "0.4.3" libc = "0.2.103" tar = "0.4.37" diff --git a/src/archive/tar.rs b/src/archive/tar.rs index a5fa6f0..96f93e6 100644 --- a/src/archive/tar.rs +++ b/src/archive/tar.rs @@ -10,11 +10,15 @@ use tar; use walkdir::WalkDir; use crate::{ - info, oof, + info, utils::{self, Bytes}, }; -pub fn unpack_archive(reader: Box, output_folder: &Path, flags: &oof::Flags) -> crate::Result> { +pub fn unpack_archive( + reader: Box, + output_folder: &Path, + skip_questions_positively: Option, +) -> crate::Result> { let mut archive = tar::Archive::new(reader); let mut files_unpacked = vec![]; @@ -22,7 +26,7 @@ pub fn unpack_archive(reader: Box, output_folder: &Path, flags: &oof:: let mut file = file?; let file_path = output_folder.join(file.path()?); - if file_path.exists() && !utils::user_wants_to_overwrite(&file_path, flags)? { + if file_path.exists() && !utils::user_wants_to_overwrite(&file_path, skip_questions_positively)? { continue; } diff --git a/src/archive/zip.rs b/src/archive/zip.rs index be47ea8..eb2187e 100644 --- a/src/archive/zip.rs +++ b/src/archive/zip.rs @@ -10,14 +10,18 @@ use walkdir::WalkDir; use zip::{self, read::ZipFile, ZipArchive}; use crate::{ - info, oof, + info, utils::{self, dir_is_empty, Bytes}, }; use self::utf8::get_invalid_utf8_paths; /// Unpacks the archive given by `archive` into the folder given by `into`. -pub fn unpack_archive(mut archive: ZipArchive, into: &Path, flags: &oof::Flags) -> crate::Result> +pub fn unpack_archive( + mut archive: ZipArchive, + into: &Path, + skip_questions_positively: Option, +) -> crate::Result> where R: Read + Seek, { @@ -30,7 +34,7 @@ where }; let file_path = into.join(file_path); - if file_path.exists() && !utils::user_wants_to_overwrite(&file_path, flags)? { + if file_path.exists() && !utils::user_wants_to_overwrite(&file_path, skip_questions_positively)? { continue; } diff --git a/src/cli.rs b/src/cli.rs index d507775..ed880ae 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,81 +1,75 @@ -//! CLI argparser configuration, command detection and input treatment. -//! -//! NOTE: the argparser implementation itself is not in this file. +//! CLI arg parser configuration, command detection and input treatment. use std::{ env, - ffi::OsString, path::{Path, PathBuf}, vec::Vec, }; -use strsim::normalized_damerau_levenshtein; +use clap::{crate_authors, crate_description, crate_name, crate_version, Parser, ValueHint}; -use crate::{arg_flag, error::FinalError, flag, oof, Error}; +use crate::Error; -#[derive(PartialEq, Eq, Debug)] -pub enum Command { - /// Files to be compressed +#[derive(Parser, Debug)] +#[clap(name = crate_name!(), version = crate_version!(), author = crate_authors!(), about = crate_description!())] +pub struct Opts { + /// Skip overwrite questions positively. + #[clap(short, long, conflicts_with = "no")] + pub yes: bool, + + /// Skip overwrite questions negatively. + #[clap(short, long)] + pub no: bool, + + #[clap(subcommand)] + pub cmd: Subcommand, +} + +#[derive(Parser, PartialEq, Eq, Debug)] +pub enum Subcommand { + /// Compress files. Alias: c + #[clap(alias = "c")] Compress { + /// Files to be compressed + #[clap(required = true, min_values = 1)] files: Vec, - output_path: PathBuf, + + /// The resulting file. Its extensions specify how the files will be compressed and they need to be supported + #[clap(required = true, value_hint = ValueHint::FilePath)] + output: PathBuf, }, - /// Files to be decompressed and their extensions + /// Compress files. Alias: d + #[clap(alias = "d")] Decompress { + /// Files to be decompressed + #[clap(required = true, min_values = 1)] files: Vec, - output_folder: Option, + + /// Decompress files in a directory other than the current + #[clap(short, long, value_hint = ValueHint::DirPath)] + output: Option, }, - ShowHelp, - ShowVersion, } -/// Calls parse_args_and_flags_from using argv (std::env::args_os) -/// -/// This function is also responsible for treating and checking the command-line input, -/// such as calling [`canonicalize`](std::fs::canonicalize), checking if it the given files exists, etc. -pub fn parse_args() -> crate::Result { - // From argv, but ignoring empty arguments - let args = env::args_os().skip(1).filter(|arg| !arg.is_empty()).collect(); - let mut parsed_args = parse_args_from(args)?; +impl Opts { + /// A helper method that calls `clap::Parser::parse` and then translates relative paths to absolute. + /// Also determines if the user wants to skip questions or not + pub fn parse_args() -> crate::Result<(Self, Option)> { + let mut opts: Self = Self::parse(); - // If has a list of files, canonicalize them, reporting error if they do not exist - match &mut parsed_args.command { - Command::Compress { files, .. } | Command::Decompress { files, .. } => { - *files = canonicalize_files(files)?; - } - _ => {} + let (Subcommand::Compress { files, .. } | Subcommand::Decompress { files, .. }) = &mut opts.cmd; + *files = canonicalize_files(files)?; + + let skip_questions_positively = if opts.yes { + Some(true) + } else if opts.no { + Some(false) + } else { + None + }; + + Ok((opts, skip_questions_positively)) } - - if parsed_args.flags.is_present("yes") && parsed_args.flags.is_present("no") { - return Err(Error::Custom { - reason: FinalError::with_title("Conflicted flags detected.") - .detail("You can't use both --yes and --no at the same time.") - .hint("Use --yes if you want to positively skip overwrite questions") - .hint("Use --no if you want to negatively skip overwrite questions") - .hint("Don't use either if you want to be asked each time"), - }); - } - - Ok(parsed_args) -} - -#[derive(Debug)] -pub struct ParsedArgs { - pub command: Command, - pub flags: oof::Flags, -} - -/// Checks if the first argument is a typo for the `compress` subcommand. -/// Returns true if the arg is probably a typo or false otherwise. -fn is_typo(path: impl AsRef) -> bool { - if path.as_ref().exists() { - // If the file exists then we won't check for a typo - return false; - } - - let path = path.as_ref().to_string_lossy(); - // We'll consider it a typo if the word is somewhat 'close' to "compress" - normalized_damerau_levenshtein("compress", &path) > 0.625 } fn canonicalize(path: impl AsRef) -> crate::Result { @@ -94,120 +88,3 @@ fn canonicalize(path: impl AsRef) -> crate::Result { fn canonicalize_files(files: &[impl AsRef]) -> crate::Result> { files.iter().map(canonicalize).collect() } - -pub fn parse_args_from(mut args: Vec) -> crate::Result { - if oof::matches_any_arg(&args, &["--help", "-h"]) || args.is_empty() { - return Ok(ParsedArgs { command: Command::ShowHelp, flags: oof::Flags::default() }); - } - - if oof::matches_any_arg(&args, &["--version"]) { - return Ok(ParsedArgs { command: Command::ShowVersion, flags: oof::Flags::default() }); - } - - let subcommands = &["c", "compress", "d", "decompress"]; - let mut flags_info = vec![flag!('y', "yes"), flag!('n', "no")]; - - let parsed_args = match oof::pop_subcommand(&mut args, subcommands) { - Some(&"c") | Some(&"compress") => { - // `ouch compress` subcommand - let (args, flags) = oof::filter_flags(args, &flags_info)?; - let mut files: Vec = args.into_iter().map(PathBuf::from).collect(); - - if files.len() < 2 { - return Err(Error::MissingArgumentsForCompression); - } - - // Safety: we checked that args.len() >= 2 - let output_path = files.pop().unwrap(); - - let command = Command::Compress { files, output_path }; - ParsedArgs { command, flags } - } - Some(&"d") | Some(&"decompress") => { - flags_info.push(arg_flag!('o', "output")); - - if let Some(first_arg) = args.first() { - if is_typo(first_arg) { - return Err(Error::CompressionTypo); - } - } else { - return Err(Error::MissingArgumentsForDecompression); - } - - // Parse flags - let (files, flags) = oof::filter_flags(args, &flags_info)?; - let files = files.into_iter().map(PathBuf::from).collect(); - - let output_folder = flags.arg("output").map(PathBuf::from); - - // TODO: ensure all files are decompressible - - let command = Command::Decompress { files, output_folder }; - ParsedArgs { command, flags } - } - // Defaults to help when there is no subcommand - None => { - return Ok(ParsedArgs { command: Command::ShowHelp, flags: oof::Flags::default() }); - } - _ => unreachable!("You should match each subcommand passed."), - }; - - Ok(parsed_args) -} - -#[cfg(test)] -mod tests { - - use super::*; - - fn gen_args>(text: &str) -> Vec { - let args = text.split_whitespace(); - args.map(OsString::from).map(T::from).collect() - } - - fn test_cli(args: &str) -> crate::Result { - let args = gen_args(args); - parse_args_from(args) - } - - #[test] - fn test_cli_commands() { - assert_eq!(test_cli("").unwrap().command, Command::ShowHelp); - assert_eq!(test_cli("--help").unwrap().command, Command::ShowHelp); - assert_eq!(test_cli("--version").unwrap().command, Command::ShowVersion); - assert_eq!(test_cli("--version").unwrap().flags, oof::Flags::default()); - assert_eq!( - test_cli("decompress foo.zip bar.zip").unwrap().command, - Command::Decompress { files: gen_args("foo.zip bar.zip"), output_folder: None } - ); - assert_eq!( - test_cli("d foo.zip bar.zip").unwrap().command, - Command::Decompress { files: gen_args("foo.zip bar.zip"), output_folder: None } - ); - assert_eq!( - test_cli("compress foo bar baz.zip").unwrap().command, - Command::Compress { files: gen_args("foo bar"), output_path: "baz.zip".into() } - ); - assert_eq!( - test_cli("c foo bar baz.zip").unwrap().command, - Command::Compress { files: gen_args("foo bar"), output_path: "baz.zip".into() } - ); - assert_eq!(test_cli("compress").unwrap_err(), Error::MissingArgumentsForCompression); - // assert_eq!(test_cli("decompress").unwrap_err(), Error::MissingArgumentsForCompression); // TODO - } - - #[test] - fn test_cli_flags() { - // --help and --version flags are considered commands that are ran over anything else - assert_eq!(test_cli("--help").unwrap().flags, oof::Flags::default()); - assert_eq!(test_cli("--version").unwrap().flags, oof::Flags::default()); - - assert_eq!( - test_cli("decompress foo --yes bar --output folder").unwrap().flags, - oof::Flags { - boolean_flags: vec!["yes"].into_iter().collect(), - argument_flags: vec![("output", OsString::from("folder"))].into_iter().collect(), - } - ); - } -} diff --git a/src/commands.rs b/src/commands.rs index de2722d..7960804 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -12,13 +12,13 @@ use utils::colors; use crate::{ archive, - cli::Command, + cli::{Opts, Subcommand}, error::FinalError, extension::{ self, CompressionFormat::{self, *}, }, - info, oof, + info, utils::to_utf, utils::{self, dir_is_empty}, Error, @@ -37,9 +37,9 @@ fn represents_several_files(files: &[PathBuf]) -> bool { files.iter().any(is_non_empty_dir) || files.len() > 1 } -pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> { - match command { - Command::Compress { files, output_path } => { +pub fn run(args: Opts, skip_questions_positively: Option) -> crate::Result<()> { + match args.cmd { + Subcommand::Compress { files, output: output_path } => { // Formats from path extension, like "file.tar.gz.xz" -> vec![Tar, Gzip, Lzma] let mut formats = extension::extensions_from_path(&output_path); @@ -93,7 +93,7 @@ pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> { return Err(Error::with_reason(reason)); } - if output_path.exists() && !utils::user_wants_to_overwrite(&output_path, flags)? { + if output_path.exists() && !utils::user_wants_to_overwrite(&output_path, skip_questions_positively)? { // User does not want to overwrite this file return Ok(()); } @@ -125,7 +125,7 @@ pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> { drop(drain_iter); // Remove the extensions from `formats` } } - let compress_result = compress_files(files, formats, output_file, flags); + let compress_result = compress_files(files, formats, output_file); // If any error occurred, delete incomplete file if compress_result.is_err() { @@ -143,7 +143,7 @@ pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> { compress_result?; } - Command::Decompress { files, output_folder } => { + Subcommand::Decompress { files, output: output_folder } => { let mut output_paths = vec![]; let mut formats = vec![]; @@ -175,21 +175,14 @@ pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> { let output_folder = output_folder.as_ref().map(|path| path.as_ref()); for ((input_path, formats), file_name) in files.iter().zip(formats).zip(output_paths) { - decompress_file(input_path, formats, output_folder, file_name, flags)?; + decompress_file(input_path, formats, output_folder, file_name, skip_questions_positively)?; } } - Command::ShowHelp => crate::help_command(), - Command::ShowVersion => crate::version_command(), } Ok(()) } -fn compress_files( - files: Vec, - formats: Vec, - output_file: fs::File, - _flags: &oof::Flags, -) -> crate::Result<()> { +fn compress_files(files: Vec, formats: Vec, output_file: fs::File) -> crate::Result<()> { let file_writer = BufWriter::with_capacity(BUFFER_CAPACITY, output_file); if let [Tar | Tgz | Zip] = *formats.as_slice() { @@ -295,7 +288,7 @@ fn decompress_file( formats: Vec, output_folder: Option<&Path>, file_name: &Path, - flags: &oof::Flags, + skip_questions_positively: Option, ) -> crate::Result<()> { // TODO: improve error message let reader = fs::File::open(&input_file_path)?; @@ -317,7 +310,7 @@ fn decompress_file( if let [Zip] = *formats.as_slice() { utils::create_dir_if_non_existent(output_folder)?; let zip_archive = zip::ZipArchive::new(reader)?; - let _files = crate::archive::zip::unpack_archive(zip_archive, output_folder, flags)?; + let _files = crate::archive::zip::unpack_archive(zip_archive, output_folder, skip_questions_positively)?; info!("Successfully uncompressed archive in '{}'.", to_utf(output_folder)); return Ok(()); } @@ -355,27 +348,27 @@ fn decompress_file( info!("Successfully uncompressed archive in '{}'.", to_utf(output_path)); } Tar => { - let _ = crate::archive::tar::unpack_archive(reader, output_folder, flags)?; + let _ = crate::archive::tar::unpack_archive(reader, output_folder, skip_questions_positively)?; info!("Successfully uncompressed archive in '{}'.", to_utf(output_folder)); } Tgz => { let reader = chain_reader_decoder(&Gzip, reader)?; - let _ = crate::archive::tar::unpack_archive(reader, output_folder, flags)?; + let _ = crate::archive::tar::unpack_archive(reader, output_folder, skip_questions_positively)?; info!("Successfully uncompressed archive in '{}'.", to_utf(output_folder)); } Tbz => { let reader = chain_reader_decoder(&Bzip, reader)?; - let _ = crate::archive::tar::unpack_archive(reader, output_folder, flags)?; + let _ = crate::archive::tar::unpack_archive(reader, output_folder, skip_questions_positively)?; info!("Successfully uncompressed archive in '{}'.", to_utf(output_folder)); } Tlzma => { let reader = chain_reader_decoder(&Lzma, reader)?; - let _ = crate::archive::tar::unpack_archive(reader, output_folder, flags)?; + let _ = crate::archive::tar::unpack_archive(reader, output_folder, skip_questions_positively)?; info!("Successfully uncompressed archive in '{}'.", to_utf(output_folder)); } Tzst => { let reader = chain_reader_decoder(&Zstd, reader)?; - let _ = crate::archive::tar::unpack_archive(reader, output_folder, flags)?; + let _ = crate::archive::tar::unpack_archive(reader, output_folder, skip_questions_positively)?; info!("Successfully uncompressed archive in '{}'.", to_utf(output_folder)); } Zip => { @@ -390,7 +383,7 @@ fn decompress_file( io::copy(&mut reader, &mut vec)?; let zip_archive = zip::ZipArchive::new(io::Cursor::new(vec))?; - let _ = crate::archive::zip::unpack_archive(zip_archive, output_folder, flags)?; + let _ = crate::archive::zip::unpack_archive(zip_archive, output_folder, skip_questions_positively)?; info!("Successfully uncompressed archive in '{}'.", to_utf(output_folder)); } diff --git a/src/error.rs b/src/error.rs index 2916089..a8dd654 100644 --- a/src/error.rs +++ b/src/error.rs @@ -11,7 +11,7 @@ use std::{ path::{Path, PathBuf}, }; -use crate::{oof, utils::colors::*}; +use crate::utils::colors::*; #[derive(Debug, PartialEq)] pub enum Error { @@ -24,7 +24,6 @@ pub enum Error { PermissionDenied, UnsupportedZipArchive(&'static str), InternalError, - OofError(oof::OofError), CompressingRootFolder, MissingArgumentsForCompression, MissingArgumentsForDecompression, @@ -127,7 +126,6 @@ impl fmt::Display for Error { .detail("Please help us improve by reporting the issue at:") .detail(format!(" {}https://github.com/vrmiguel/ouch/issues ", *CYAN)) } - Error::OofError(err) => FinalError::with_title(err), Error::IoError { reason } => FinalError::with_title(reason), Error::CompressionTypo => { FinalError::with_title("Possible typo detected") @@ -179,9 +177,3 @@ impl From for Error { Self::WalkdirError { reason: err.to_string() } } } - -impl From for Error { - fn from(err: oof::OofError) -> Self { - Self::OofError(err) - } -} diff --git a/src/lib.rs b/src/lib.rs index 3c3830d..9a1a795 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,6 @@ // Public modules pub mod cli; pub mod commands; -pub mod oof; // Private modules pub mod archive; @@ -21,48 +20,3 @@ pub use error::{Error, Result}; /// The status code ouch has when an error is encountered pub const EXIT_FAILURE: i32 = libc::EXIT_FAILURE; - -const VERSION: &str = env!("CARGO_PKG_VERSION"); - -fn help_command() { - use utils::colors::*; - - println!( - "\ -{cyan}ouch{reset} - Obvious Unified Compression files Helper - -{cyan}USAGE:{reset} - {green}ouch decompress {magenta}{reset} Decompresses files. - - {green}ouch compress {magenta} OUTPUT.EXT{reset} Compresses files into {magenta}OUTPUT.EXT{reset}, - where {magenta}EXT{reset} must be a supported format. - -{cyan}ALIASES:{reset} - {green}d decompress {reset} - {green}c compress {reset} - -{cyan}FLAGS:{reset} - {yellow}-h{white}, {yellow}--help{reset} Display this help information. - {yellow}-y{white}, {yellow}--yes{reset} Skip overwrite questions. - {yellow}-n{white}, {yellow}--no{reset} Skip overwrite questions. - {yellow}--version{reset} Display version information. - -{cyan}SPECIFIC FLAGS:{reset} - {yellow}-o{reset}, {yellow}--output{reset} FOLDER_PATH When decompressing, to decompress files to - another folder. - -Visit https://github.com/ouch-org/ouch for more usage examples.", - magenta = *MAGENTA, - white = *WHITE, - green = *GREEN, - yellow = *YELLOW, - reset = *RESET, - cyan = *CYAN - ); -} - -#[inline] -fn version_command() { - use utils::colors::*; - println!("{green}ouch{reset} {}", crate::VERSION, green = *GREEN, reset = *RESET); -} diff --git a/src/main.rs b/src/main.rs index 57a5c60..9fc88ec 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,4 @@ -use ouch::{ - cli::{parse_args, ParsedArgs}, - commands, Result, -}; +use ouch::{cli::Opts, commands, Result}; fn main() { if let Err(err) = run() { @@ -10,7 +7,7 @@ fn main() { } } -fn run() -> crate::Result<()> { - let ParsedArgs { command, flags } = parse_args()?; - commands::run(command, &flags) +fn run() -> Result<()> { + let (args, skip_questions_positively) = Opts::parse_args()?; + commands::run(args, skip_questions_positively) } diff --git a/src/oof/error.rs b/src/oof/error.rs deleted file mode 100644 index 299601a..0000000 --- a/src/oof/error.rs +++ /dev/null @@ -1,55 +0,0 @@ -//! Errors related to argparsing. - -use std::{error, ffi::OsString, fmt}; - -use super::Flag; - -#[derive(Debug, PartialEq)] -pub enum OofError { - FlagValueConflict { - flag: Flag, - previous_value: OsString, - new_value: OsString, - }, - /// User supplied a flag containing invalid Unicode - InvalidUnicode(OsString), - /// User supplied an unrecognized short flag - UnknownShortFlag(char), - UnknownLongFlag(String), - MisplacedShortArgFlagError(char), - MissingValueToFlag(Flag), - DuplicatedFlag(Flag), -} - -impl error::Error for OofError { - fn source(&self) -> Option<&(dyn error::Error + 'static)> { - None - } -} - -impl fmt::Display for OofError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - // TODO: implement proper debug messages - match self { - OofError::FlagValueConflict { flag, previous_value, new_value } => { - write!( - f, - "CLI flag value conflicted for flag '--{}', previous: {:?}, new: {:?}.", - flag.long, previous_value, new_value - ) - } - OofError::InvalidUnicode(flag) => write!(f, "{:?} is not valid Unicode.", flag), - OofError::UnknownShortFlag(ch) => write!(f, "Unknown argument '-{}'", ch), - OofError::MisplacedShortArgFlagError(ch) => { - write!( - f, - "Invalid placement of `-{}`.\nOnly the last letter in a sequence of short flags can take values.", - ch - ) - } - OofError::MissingValueToFlag(flag) => write!(f, "Flag {} takes value but none was supplied.", flag), - OofError::DuplicatedFlag(flag) => write!(f, "Duplicated usage of {}.", flag), - OofError::UnknownLongFlag(flag) => write!(f, "Unknown argument '--{}'", flag), - } - } -} diff --git a/src/oof/flags.rs b/src/oof/flags.rs deleted file mode 100644 index 231ec5c..0000000 --- a/src/oof/flags.rs +++ /dev/null @@ -1,109 +0,0 @@ -use std::{ - collections::{HashMap, HashSet}, - ffi::{OsStr, OsString}, -}; - -/// Shallow type, created to indicate a `Flag` that accepts a argument. -/// -/// ArgFlag::long(), is actually a Flag::long(), but sets a internal attribute. -/// -/// Examples in here pls -#[derive(Debug)] -pub struct ArgFlag; - -impl ArgFlag { - pub fn long(name: &'static str) -> Flag { - Flag { long: name, short: None, takes_value: true } - } -} - -#[derive(Debug, PartialEq, Clone)] -pub struct Flag { - // Also the name - pub long: &'static str, - pub short: Option, - pub takes_value: bool, -} - -impl std::fmt::Display for Flag { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self.short { - Some(short_flag) => write!(f, "-{}/--{}", short_flag, self.long), - None => write!(f, "--{}", self.long), - } - } -} - -impl Flag { - pub fn long(name: &'static str) -> Self { - Self { long: name, short: None, takes_value: false } - } - - pub fn short(mut self, short_flag_char: char) -> Self { - self.short = Some(short_flag_char); - self - } -} - -#[derive(Default, PartialEq, Eq, Debug)] -pub struct Flags { - pub boolean_flags: HashSet<&'static str>, - pub argument_flags: HashMap<&'static str, OsString>, -} - -impl Flags { - pub fn new() -> Self { - Self::default() - } - - pub fn is_present(&self, flag_name: &str) -> bool { - self.boolean_flags.contains(flag_name) || self.argument_flags.contains_key(flag_name) - } - - pub fn arg(&self, flag_name: &str) -> Option<&OsString> { - self.argument_flags.get(flag_name) - } - - pub fn take_arg(&mut self, flag_name: &str) -> Option { - self.argument_flags.remove(flag_name) - } -} - -#[derive(Debug)] -pub enum FlagType { - None, - Short, - Long, -} - -impl FlagType { - pub fn from(text: impl AsRef) -> Self { - let text = text.as_ref(); - - let mut iter; - - #[cfg(target_family = "unix")] - { - use std::os::unix::ffi::OsStrExt; - iter = text.as_bytes().iter(); - } - #[cfg(target_family = "windows")] - { - use std::os::windows::ffi::OsStrExt; - iter = text.encode_wide(); - } - - // 45 is the code for a hyphen - // Typed as 45_u16 for Windows - // Typed as 45_u8 for Unix - if let Some(45) = iter.next() { - if let Some(45) = iter.next() { - Self::Long - } else { - Self::Short - } - } else { - Self::None - } - } -} diff --git a/src/oof/mod.rs b/src/oof/mod.rs deleted file mode 100644 index 788113b..0000000 --- a/src/oof/mod.rs +++ /dev/null @@ -1,383 +0,0 @@ -//! Ouch's argparsing crate. -//! -//! The usage of this crate is heavily based on boolean_flags and -//! argument_flags, there should be an more _obvious_ naming. - -mod error; -mod flags; -pub mod util; - -use std::{ - collections::BTreeMap, - ffi::{OsStr, OsString}, -}; - -pub use error::OofError; -pub use flags::{ArgFlag, Flag, FlagType, Flags}; -use util::trim_double_hyphen; - -/// Pop leading application `subcommand`, if valid. -/// -/// `args` can be a Vec of `OsString` or `OsStr` -/// `subcommands` is any container that can yield `&str` through `AsRef`, can be `Vec<&str>` or -/// a GREAT `BTreeSet` (or `BTreeSet<&str>`). -pub fn pop_subcommand<'a, T, I, II>(args: &mut Vec, subcommands: I) -> Option<&'a II> -where - I: IntoIterator, - II: AsRef, - T: AsRef, -{ - if args.is_empty() { - return None; - } - - for subcommand in subcommands.into_iter() { - if subcommand.as_ref() == args[0].as_ref() { - args.remove(0); - return Some(subcommand); - } - } - - None -} - -/// Detect flags from args and filter from args. -/// -/// Each flag received via flags_info should must have unique long and short identifiers. -/// -/// # Panics (Developer errors) -/// - If there are duplicated short flag identifiers. -/// - If there are duplicated long flag identifiers. -/// -/// Both conditions cause panic because your program's flags specification is meant to have unique -/// flags. There shouldn't be two "--verbose" flags, for example. -/// Caller should guarantee it, fortunately, this can almost always be caught while prototyping in -/// debug mode, test your CLI flags once, if it works once, you're good, -/// -/// # Errors (User errors) -/// - Argument flag comes at last arg, so there's no way to provide an argument. -/// - Or if it doesn't comes at last, but the rest are just flags, no possible and valid arg. -/// - Short flags with multiple letters in the same arg contain a argument flag that does not come -/// as the last one in the list (example "-oahc", where 'o', 'a', or 'h' is a argument flag, but do -/// not comes at last, so it is impossible for them to receive the required argument. -/// - User passes same flag twice (short or long, boolean or arg). -/// -/// ... -pub fn filter_flags(args: Vec, flags_info: &[Flag]) -> Result<(Vec, Flags), OofError> { - let mut short_flags_info = BTreeMap::::new(); - let mut long_flags_info = BTreeMap::<&'static str, &Flag>::new(); - - for flag in flags_info.iter() { - // Panics if duplicated/conflicts - assert!(!long_flags_info.contains_key(flag.long), "DEV ERROR: duplicated long flag '{}'.", flag.long); - - long_flags_info.insert(flag.long, flag); - - if let Some(short) = flag.short { - // Panics if duplicated/conflicts - assert!(!short_flags_info.contains_key(&short), "DEV ERROR: duplicated short flag '-{}'.", short); - short_flags_info.insert(short, flag); - } - } - - // Consume args, filter out flags, and add back to args new vec - let mut iter = args.into_iter(); - let mut new_args = vec![]; - let mut result_flags = Flags::new(); - - while let Some(arg) = iter.next() { - let flag_type = FlagType::from(&arg); - - // If it isn't a flag, retrieve to `args` and skip this iteration - if let FlagType::None = flag_type { - new_args.push(arg); - continue; - } - - // If it is a flag, now we try to interpret it as valid utf-8 - let flag = match arg.to_str() { - Some(arg) => arg, - None => return Err(OofError::InvalidUnicode(arg)), - }; - - // Only one hyphen in the flag - // A short flag can be of form "-", "-abcd", "-h", "-v", etc - if let FlagType::Short = flag_type { - assert_eq!(flag.chars().next(), Some('-')); - - // TODO - // TODO: what should happen if the flag is empty????? - // if flags.chars().skip(1).next().is_none() { - // panic!("User error: flag is empty???"); - // } - - // Skip hyphen and get all letters - let letters = flag.chars().skip(1).collect::>(); - - // For each letter in the short arg, except the last one - for (i, letter) in letters.iter().copied().enumerate() { - // Safety: this loop only runs when len >= 1, so this subtraction is safe - let is_last_letter = i == letters.len() - 1; - - let flag_info = *short_flags_info.get(&letter).ok_or(OofError::UnknownShortFlag(letter))?; - - if !is_last_letter && flag_info.takes_value { - return Err(OofError::MisplacedShortArgFlagError(letter)); - // Because "-AB argument" only works if B takes values, not A. - // That is, the short flag that takes values needs to come at the end - // of this piece of text - } - - let flag_name: &'static str = flag_info.long; - - if flag_info.takes_value { - // If it was already inserted - if result_flags.argument_flags.contains_key(flag_name) { - return Err(OofError::DuplicatedFlag(flag_info.clone())); - } - - // pop the next one - let flag_argument = iter.next().ok_or_else(|| OofError::MissingValueToFlag(flag_info.clone()))?; - - // Otherwise, insert it. - result_flags.argument_flags.insert(flag_name, flag_argument); - } else { - // If it was already inserted - if result_flags.boolean_flags.contains(flag_name) { - return Err(OofError::DuplicatedFlag(flag_info.clone())); - } - // Otherwise, insert it - result_flags.boolean_flags.insert(flag_name); - } - } - } - - if let FlagType::Long = flag_type { - let flag = trim_double_hyphen(flag); - - let flag_info = *long_flags_info.get(flag).ok_or_else(|| OofError::UnknownLongFlag(String::from(flag)))?; - - let flag_name = flag_info.long; - - if flag_info.takes_value { - // If it was already inserted - if result_flags.argument_flags.contains_key(&flag_name) { - return Err(OofError::DuplicatedFlag(flag_info.clone())); - } - - let flag_argument = iter.next().ok_or_else(|| OofError::MissingValueToFlag(flag_info.clone()))?; - result_flags.argument_flags.insert(flag_name, flag_argument); - } else { - // If it was already inserted - if result_flags.boolean_flags.contains(&flag_name) { - return Err(OofError::DuplicatedFlag(flag_info.clone())); - } - // Otherwise, insert it - result_flags.boolean_flags.insert(flag_name); - } - - // // TODO - // TODO: what should happen if the flag is empty????? - // if flag.is_empty() { - // panic!("Is this an error?"); - // } - } - } - - Ok((new_args, result_flags)) -} - -/// Says if any text matches any arg -pub fn matches_any_arg(args: &[T], texts: &[U]) -> bool -where - T: AsRef, - U: AsRef, -{ - texts.iter().any(|text| args.iter().any(|arg| arg.as_ref() == text.as_ref())) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn gen_args(text: &str) -> Vec { - let args = text.split_whitespace(); - args.map(OsString::from).collect() - } - - fn setup_args_scenario(arg_str: &str) -> Result<(Vec, Flags), OofError> { - let flags_info = - [ArgFlag::long("output_file").short('o'), Flag::long("verbose").short('v'), Flag::long("help").short('h')]; - - let args = gen_args(arg_str); - filter_flags(args, &flags_info) - } - - #[test] - fn test_unknown_flags() { - let result = setup_args_scenario("ouch a.zip -s b.tar.gz c.tar").unwrap_err(); - assert!(matches!(result, OofError::UnknownShortFlag(flag) if flag == 's')); - - let unknown_long_flag = "foobar".to_string(); - let result = setup_args_scenario("ouch a.zip --foobar b.tar.gz c.tar").unwrap_err(); - - assert!(matches!(result, OofError::UnknownLongFlag(flag) if flag == unknown_long_flag)); - } - - #[test] - fn test_incomplete_flags() { - let incomplete_flag = ArgFlag::long("output_file").short('o'); - let result = setup_args_scenario("ouch a.zip b.tar.gz c.tar -o").unwrap_err(); - - assert!(matches!(result, OofError::MissingValueToFlag(flag) if flag == incomplete_flag)); - } - - #[test] - fn test_duplicated_flags() { - let duplicated_flag = ArgFlag::long("output_file").short('o'); - let result = setup_args_scenario("ouch a.zip b.tar.gz c.tar -o -o -o").unwrap_err(); - - assert!(matches!(result, OofError::DuplicatedFlag(flag) if flag == duplicated_flag)); - } - - #[test] - fn test_misplaced_flag() { - let misplaced_flag = ArgFlag::long("output_file").short('o'); - let result = setup_args_scenario("ouch -ov a.zip b.tar.gz c.tar").unwrap_err(); - - assert!(matches!(result, OofError::MisplacedShortArgFlagError(flag) if flag == misplaced_flag.short.unwrap())); - } - - // #[test] - // fn test_invalid_unicode_flag() { - // use std::os::unix::prelude::OsStringExt; - - // // `invalid_unicode_flag` has to contain a leading hyphen to be considered a flag. - // let invalid_unicode_flag = OsString::from_vec(vec![45, 0, 0, 0, 255, 255, 255, 255]); - // let result = filter_flags(vec![invalid_unicode_flag.clone()], &[]).unwrap_err(); - - // assert!(matches!(result, OofError::InvalidUnicode(flag) if flag == invalid_unicode_flag)); - // } - - // asdasdsa - #[test] - fn test_filter_flags() { - let flags_info = - [ArgFlag::long("output_file").short('o'), Flag::long("verbose").short('v'), Flag::long("help").short('h')]; - let args = gen_args("ouch a.zip -v b.tar.gz --output_file new_folder c.tar"); - - let (args, mut flags) = filter_flags(args, &flags_info).unwrap(); - - assert_eq!(args, gen_args("ouch a.zip b.tar.gz c.tar")); - assert!(flags.is_present("output_file")); - assert_eq!(Some(&OsString::from("new_folder")), flags.arg("output_file")); - assert_eq!(Some(OsString::from("new_folder")), flags.take_arg("output_file")); - assert!(!flags.is_present("output_file")); - } - - #[test] - fn test_pop_subcommand() { - let subcommands = &["commit", "add", "push", "remote"]; - let mut args = gen_args("add a b c"); - - let result = pop_subcommand(&mut args, subcommands); - - assert_eq!(result, Some(&"add")); - assert_eq!(args[0], "a"); - - // Check when no subcommand matches - let mut args = gen_args("a b c"); - let result = pop_subcommand(&mut args, subcommands); - - assert_eq!(result, None); - assert_eq!(args[0], "a"); - } - - // #[test] - // fn test_flag_info_macros() { - // let flags_info = [ - // arg_flag!('o', "output_file"), - // arg_flag!("delay"), - // flag!('v', "verbose"), - // flag!('h', "help"), - // flag!("version"), - // ]; - - // let expected = [ - // ArgFlag::long("output_file").short('o'), - // ArgFlag::long("delay"), - // Flag::long("verbose").short('v'), - // Flag::long("help").short('h'), - // Flag::long("version"), - // ]; - - // assert_eq!(flags_info, expected); - // } - - #[test] - // TODO: remove should_panic and use proper error handling inside of filter_args - #[should_panic] - fn test_flag_info_with_long_flag_conflict() { - let flags_info = [ArgFlag::long("verbose").short('a'), Flag::long("verbose").short('b')]; - - // Should panic here - let result = filter_flags(vec![], &flags_info); - assert!(matches!(result, Err(OofError::FlagValueConflict { .. }))); - } - - #[test] - // TODO: remove should_panic and use proper error handling inside of filter_args - #[should_panic] - fn test_flag_info_with_short_flag_conflict() { - let flags_info = [ArgFlag::long("output_file").short('o'), Flag::long("verbose").short('o')]; - - // Should panic here - filter_flags(vec![], &flags_info).unwrap_err(); - } - - #[test] - fn test_matches_any_arg_function() { - let args = gen_args("program a -h b"); - assert!(matches_any_arg(&args, &["--help", "-h"])); - - let args = gen_args("program a b --help"); - assert!(matches_any_arg(&args, &["--help", "-h"])); - - let args = gen_args("--version program a b"); - assert!(matches_any_arg(&args, &["--version", "-v"])); - - let args = gen_args("program -v a --version b"); - assert!(matches_any_arg(&args, &["--version", "-v"])); - - // Cases without it - let args = gen_args("program a b c"); - assert!(!matches_any_arg(&args, &["--help", "-h"])); - - let args = gen_args("program a --version -v b c"); - assert!(!matches_any_arg(&args, &["--help", "-h"])); - } -} - -/// Create a flag with long flag (?). -#[macro_export] -macro_rules! flag { - ($short:expr, $long:expr) => {{ - oof::Flag::long($long).short($short) - }}; - - ($long:expr) => {{ - oof::Flag::long($long) - }}; -} - -/// Create a flag with long flag (?), receives argument (?). -#[macro_export] -macro_rules! arg_flag { - ($short:expr, $long:expr) => {{ - oof::ArgFlag::long($long).short($short) - }}; - - ($long:expr) => {{ - oof::ArgFlag::long($long) - }}; -} diff --git a/src/oof/util.rs b/src/oof/util.rs deleted file mode 100644 index 0e75350..0000000 --- a/src/oof/util.rs +++ /dev/null @@ -1,16 +0,0 @@ -/// Util function to skip the two leading long flag hyphens. -pub fn trim_double_hyphen(flag_text: &str) -> &str { - flag_text.get(2..).unwrap_or_default() -} - -#[cfg(test)] -mod tests { - use super::trim_double_hyphen; - - #[test] - fn _trim_double_hyphen() { - assert_eq!(trim_double_hyphen("--flag"), "flag"); - assert_eq!(trim_double_hyphen("--verbose"), "verbose"); - assert_eq!(trim_double_hyphen("--help"), "help"); - } -} diff --git a/src/utils.rs b/src/utils.rs index 71e0337..6e282fa 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -5,7 +5,7 @@ use std::{ path::{Path, PathBuf}, }; -use crate::{dialogs::Confirmation, info, oof}; +use crate::{dialogs::Confirmation, info}; /// Checks if the given path represents an empty directory. pub fn dir_is_empty(dir_path: &Path) -> bool { @@ -35,17 +35,12 @@ pub fn cd_into_same_dir_as(filename: &Path) -> crate::Result { Ok(previous_location) } -pub fn user_wants_to_overwrite(path: &Path, flags: &oof::Flags) -> crate::Result { - match (flags.is_present("yes"), flags.is_present("no")) { - (true, true) => { - unreachable!("This should've been cutted out in the ~/src/cli.rs filter flags function.") - } - (true, _) => return Ok(true), - (_, true) => return Ok(false), - _ => {} +pub fn user_wants_to_overwrite(path: &Path, skip_questions_positively: Option) -> crate::Result { + match skip_questions_positively { + Some(true) => Ok(true), + Some(false) => Ok(false), + None => Confirmation::new("Do you want to overwrite 'FILE'?", Some("FILE")).ask(Some(&to_utf(path))), } - - Confirmation::new("Do you want to overwrite 'FILE'?", Some("FILE")).ask(Some(&to_utf(path))) } pub fn to_utf(os_str: impl AsRef) -> String { From bc33ccc99cd0673e882fad7720b9df128516792e Mon Sep 17 00:00:00 2001 From: Spyros Roum Date: Tue, 19 Oct 2021 14:55:37 +0300 Subject: [PATCH 26/36] Update tests --- tests/compress_and_decompress.rs | 17 ++++++++++++----- tests/utils.rs | 25 ++++++++++++++++++------- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/tests/compress_and_decompress.rs b/tests/compress_and_decompress.rs index cd24b74..77d929d 100644 --- a/tests/compress_and_decompress.rs +++ b/tests/compress_and_decompress.rs @@ -7,7 +7,10 @@ use std::{ time::Duration, }; -use ouch::{cli::Command, commands::run, oof}; +use ouch::{ + cli::{Opts, Subcommand}, + commands::run, +}; use rand::{rngs::SmallRng, RngCore, SeedableRng}; use tempfile::NamedTempFile; use utils::*; @@ -172,11 +175,15 @@ fn extract_files(archive_path: &Path) -> Vec { // Add the suffix "results" extraction_output_folder.push("extraction_results"); - let command = Command::Decompress { - files: vec![archive_path.to_owned()], - output_folder: Some(extraction_output_folder.clone()), + let command = Opts { + yes: false, + no: false, + cmd: Subcommand::Decompress { + files: vec![archive_path.to_owned()], + output: Some(extraction_output_folder.clone()), + }, }; - run(command, &oof::Flags::default()).expect("Failed to extract"); + run(command, None).expect("Failed to extract"); fs::read_dir(extraction_output_folder).unwrap().map(Result::unwrap).map(|entry| entry.path()).collect() } diff --git a/tests/utils.rs b/tests/utils.rs index 05b4b0f..10cc749 100644 --- a/tests/utils.rs +++ b/tests/utils.rs @@ -7,7 +7,10 @@ use std::{ path::{Path, PathBuf}, }; -use ouch::{cli::Command, commands::run, oof}; +use ouch::{ + cli::{Opts, Subcommand}, + commands::run, +}; pub fn create_empty_dir(at: &Path, filename: &str) -> PathBuf { let dirname = Path::new(filename); @@ -22,8 +25,12 @@ pub fn compress_files(at: &Path, paths_to_compress: &[PathBuf], format: &str) -> let archive_path = String::from("archive.") + format; let archive_path = at.join(archive_path); - let command = Command::Compress { files: paths_to_compress.to_vec(), output_path: archive_path.clone() }; - run(command, &oof::Flags::default()).expect("Failed to compress test dummy files"); + let command = Opts { + yes: false, + no: false, + cmd: Subcommand::Compress { files: paths_to_compress.to_vec(), output: archive_path.clone() }, + }; + run(command, None).expect("Failed to compress test dummy files"); archive_path } @@ -40,11 +47,15 @@ pub fn extract_files(archive_path: &Path) -> Vec { // Add the suffix "results" extraction_output_folder.push("extraction_results"); - let command = Command::Decompress { - files: vec![archive_path.to_owned()], - output_folder: Some(extraction_output_folder.clone()), + let command = Opts { + yes: false, + no: false, + cmd: Subcommand::Decompress { + files: vec![archive_path.to_owned()], + output: Some(extraction_output_folder.clone()), + }, }; - run(command, &oof::Flags::default()).expect("Failed to extract"); + run(command, None).expect("Failed to extract"); fs::read_dir(extraction_output_folder).unwrap().map(Result::unwrap).map(|entry| entry.path()).collect() } From 2e6cd893dcde49fbb0cf7bb8719abf0069003edb Mon Sep 17 00:00:00 2001 From: TATSUNO Yasuhiro Date: Wed, 20 Oct 2021 00:57:11 +0900 Subject: [PATCH 27/36] Omit "./" at the start of the path (#109) --- src/archive/zip.rs | 2 ++ src/utils.rs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/archive/zip.rs b/src/archive/zip.rs index be47ea8..67d3118 100644 --- a/src/archive/zip.rs +++ b/src/archive/zip.rs @@ -3,6 +3,7 @@ use std::{ env, fs, io::{self, prelude::*}, + path::Component, path::{Path, PathBuf}, }; @@ -47,6 +48,7 @@ where fs::create_dir_all(&path)?; } } + let file_path = file_path.strip_prefix(Component::CurDir).unwrap_or_else(|_| file_path.as_path()); info!("{:?} extracted. ({})", file_path.display(), Bytes::new(file.size())); diff --git a/src/utils.rs b/src/utils.rs index 71e0337..4a5d617 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,6 +2,7 @@ use std::{ cmp, env, ffi::OsStr, fs::{self, ReadDir}, + path::Component, path::{Path, PathBuf}, }; @@ -45,6 +46,7 @@ pub fn user_wants_to_overwrite(path: &Path, flags: &oof::Flags) -> crate::Result _ => {} } + let path = path.strip_prefix(Component::CurDir).unwrap_or_else(|_| path); Confirmation::new("Do you want to overwrite 'FILE'?", Some("FILE")).ask(Some(&to_utf(path))) } From 7fb4398c04321019228422bf0facfbaaaba2abc2 Mon Sep 17 00:00:00 2001 From: Spyros Roum Date: Tue, 19 Oct 2021 19:56:27 +0300 Subject: [PATCH 28/36] Remove redundant code --- src/cli.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index ed880ae..7780b33 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,17 +1,16 @@ //! CLI arg parser configuration, command detection and input treatment. use std::{ - env, path::{Path, PathBuf}, vec::Vec, }; -use clap::{crate_authors, crate_description, crate_name, crate_version, Parser, ValueHint}; +use clap::{Parser, ValueHint}; use crate::Error; #[derive(Parser, Debug)] -#[clap(name = crate_name!(), version = crate_version!(), author = crate_authors!(), about = crate_description!())] +#[clap(version, author, about)] pub struct Opts { /// Skip overwrite questions positively. #[clap(short, long, conflicts_with = "no")] From 7b758c0ffbc8de6f8d5b87d2151398c9db2795f8 Mon Sep 17 00:00:00 2001 From: Spyros Roum Date: Tue, 19 Oct 2021 19:56:52 +0300 Subject: [PATCH 29/36] Remove author's names from help message --- src/cli.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli.rs b/src/cli.rs index 7780b33..7a13aab 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -10,7 +10,7 @@ use clap::{Parser, ValueHint}; use crate::Error; #[derive(Parser, Debug)] -#[clap(version, author, about)] +#[clap(version, about)] pub struct Opts { /// Skip overwrite questions positively. #[clap(short, long, conflicts_with = "no")] From 13aad5db87b5cfb24b577b4bcecd2cd048de5ba1 Mon Sep 17 00:00:00 2001 From: figsoda Date: Tue, 19 Oct 2021 14:13:07 -0400 Subject: [PATCH 30/36] Add repology badge to README (#113) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index cba6af6..db008da 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,8 @@ ouch compress * everything.tar.gz.xz.bz.zst.gz.gz.gz.gz.gz ## Installation +[![Packaging status](https://repology.org/badge/vertical-allrepos/ouch.svg)](https://repology.org/project/ouch/versions) + ### Downloading the latest binary Compiled for `x86_64` on _Linux_, _Mac OS_ and _Windows_, run with `curl` or `wget`. From b8a2c3899e4e90ff4fbe3fa175f301a31a514978 Mon Sep 17 00:00:00 2001 From: TATSUNO Yasuhiro Date: Wed, 20 Oct 2021 10:54:14 +0900 Subject: [PATCH 31/36] Use a same term as in command --- src/commands.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index de2722d..66408c6 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -318,7 +318,7 @@ fn decompress_file( utils::create_dir_if_non_existent(output_folder)?; let zip_archive = zip::ZipArchive::new(reader)?; let _files = crate::archive::zip::unpack_archive(zip_archive, output_folder, flags)?; - info!("Successfully uncompressed archive in '{}'.", to_utf(output_folder)); + info!("Successfully decompressed archive in '{}'.", to_utf(output_folder)); return Ok(()); } @@ -352,31 +352,31 @@ fn decompress_file( let mut writer = fs::File::create(&output_path)?; io::copy(&mut reader, &mut writer)?; - info!("Successfully uncompressed archive in '{}'.", to_utf(output_path)); + info!("Successfully decompressed archive in '{}'.", to_utf(output_path)); } Tar => { let _ = crate::archive::tar::unpack_archive(reader, output_folder, flags)?; - info!("Successfully uncompressed archive in '{}'.", to_utf(output_folder)); + info!("Successfully decompressed archive in '{}'.", to_utf(output_folder)); } Tgz => { let reader = chain_reader_decoder(&Gzip, reader)?; let _ = crate::archive::tar::unpack_archive(reader, output_folder, flags)?; - info!("Successfully uncompressed archive in '{}'.", to_utf(output_folder)); + info!("Successfully decompressed archive in '{}'.", to_utf(output_folder)); } Tbz => { let reader = chain_reader_decoder(&Bzip, reader)?; let _ = crate::archive::tar::unpack_archive(reader, output_folder, flags)?; - info!("Successfully uncompressed archive in '{}'.", to_utf(output_folder)); + info!("Successfully decompressed archive in '{}'.", to_utf(output_folder)); } Tlzma => { let reader = chain_reader_decoder(&Lzma, reader)?; let _ = crate::archive::tar::unpack_archive(reader, output_folder, flags)?; - info!("Successfully uncompressed archive in '{}'.", to_utf(output_folder)); + info!("Successfully decompressed archive in '{}'.", to_utf(output_folder)); } Tzst => { let reader = chain_reader_decoder(&Zstd, reader)?; let _ = crate::archive::tar::unpack_archive(reader, output_folder, flags)?; - info!("Successfully uncompressed archive in '{}'.", to_utf(output_folder)); + info!("Successfully decompressed archive in '{}'.", to_utf(output_folder)); } Zip => { eprintln!("Compressing first into .zip."); @@ -392,7 +392,7 @@ fn decompress_file( let _ = crate::archive::zip::unpack_archive(zip_archive, output_folder, flags)?; - info!("Successfully uncompressed archive in '{}'.", to_utf(output_folder)); + info!("Successfully decompressed archive in '{}'.", to_utf(output_folder)); } } From 5f7d77734245c0d65d88fdaf9a30178d605fa672 Mon Sep 17 00:00:00 2001 From: TATSUNO Yasuhiro Date: Wed, 20 Oct 2021 12:57:11 +0900 Subject: [PATCH 32/36] Change display of current directory (#119) --- src/commands.rs | 17 +++++++++-------- src/utils.rs | 9 +++++++++ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 66408c6..c6fc62d 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -19,6 +19,7 @@ use crate::{ CompressionFormat::{self, *}, }, info, oof, + utils::nice_directory_display, utils::to_utf, utils::{self, dir_is_empty}, Error, @@ -318,7 +319,7 @@ fn decompress_file( utils::create_dir_if_non_existent(output_folder)?; let zip_archive = zip::ZipArchive::new(reader)?; let _files = crate::archive::zip::unpack_archive(zip_archive, output_folder, flags)?; - info!("Successfully decompressed archive in '{}'.", to_utf(output_folder)); + info!("Successfully decompressed archive in {}.", nice_directory_display(output_folder)); return Ok(()); } @@ -352,31 +353,31 @@ fn decompress_file( let mut writer = fs::File::create(&output_path)?; io::copy(&mut reader, &mut writer)?; - info!("Successfully decompressed archive in '{}'.", to_utf(output_path)); + info!("Successfully decompressed archive in {}.", nice_directory_display(output_path)); } Tar => { let _ = crate::archive::tar::unpack_archive(reader, output_folder, flags)?; - info!("Successfully decompressed archive in '{}'.", to_utf(output_folder)); + info!("Successfully decompressed archive in {}.", nice_directory_display(output_folder)); } Tgz => { let reader = chain_reader_decoder(&Gzip, reader)?; let _ = crate::archive::tar::unpack_archive(reader, output_folder, flags)?; - info!("Successfully decompressed archive in '{}'.", to_utf(output_folder)); + info!("Successfully decompressed archive in {}.", nice_directory_display(output_folder)); } Tbz => { let reader = chain_reader_decoder(&Bzip, reader)?; let _ = crate::archive::tar::unpack_archive(reader, output_folder, flags)?; - info!("Successfully decompressed archive in '{}'.", to_utf(output_folder)); + info!("Successfully decompressed archive in {}.", nice_directory_display(output_folder)); } Tlzma => { let reader = chain_reader_decoder(&Lzma, reader)?; let _ = crate::archive::tar::unpack_archive(reader, output_folder, flags)?; - info!("Successfully decompressed archive in '{}'.", to_utf(output_folder)); + info!("Successfully decompressed archive in {}.", nice_directory_display(output_folder)); } Tzst => { let reader = chain_reader_decoder(&Zstd, reader)?; let _ = crate::archive::tar::unpack_archive(reader, output_folder, flags)?; - info!("Successfully decompressed archive in '{}'.", to_utf(output_folder)); + info!("Successfully decompressed archive in {}.", nice_directory_display(output_folder)); } Zip => { eprintln!("Compressing first into .zip."); @@ -392,7 +393,7 @@ fn decompress_file( let _ = crate::archive::zip::unpack_archive(zip_archive, output_folder, flags)?; - info!("Successfully decompressed archive in '{}'.", to_utf(output_folder)); + info!("Successfully decompressed archive in {}.", nice_directory_display(output_folder)); } } diff --git a/src/utils.rs b/src/utils.rs index 4a5d617..01fe9c1 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -55,6 +55,15 @@ pub fn to_utf(os_str: impl AsRef) -> String { text.trim_matches('"').to_string() } +pub fn nice_directory_display(os_str: impl AsRef) -> String { + let text = to_utf(os_str); + if text == "." { + "current directory".to_string() + } else { + format!("'{}'", text) + } +} + pub struct Bytes { bytes: f64, } From 4404b91a23f7b64c391b6745a5aa2bce7e804b9b Mon Sep 17 00:00:00 2001 From: TATSUNO Yasuhiro Date: Wed, 20 Oct 2021 13:10:10 +0900 Subject: [PATCH 33/36] refactoring: Extract function (#116) --- src/archive/zip.rs | 5 ++--- src/utils.rs | 9 ++++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/archive/zip.rs b/src/archive/zip.rs index 67d3118..f2f1d2f 100644 --- a/src/archive/zip.rs +++ b/src/archive/zip.rs @@ -3,7 +3,6 @@ use std::{ env, fs, io::{self, prelude::*}, - path::Component, path::{Path, PathBuf}, }; @@ -12,7 +11,7 @@ use zip::{self, read::ZipFile, ZipArchive}; use crate::{ info, oof, - utils::{self, dir_is_empty, Bytes}, + utils::{self, dir_is_empty, strip_cur_dir, Bytes}, }; use self::utf8::get_invalid_utf8_paths; @@ -48,7 +47,7 @@ where fs::create_dir_all(&path)?; } } - let file_path = file_path.strip_prefix(Component::CurDir).unwrap_or_else(|_| file_path.as_path()); + let file_path = strip_cur_dir(file_path.as_path()); info!("{:?} extracted. ({})", file_path.display(), Bytes::new(file.size())); diff --git a/src/utils.rs b/src/utils.rs index 01fe9c1..75ff57c 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -23,6 +23,13 @@ pub fn create_dir_if_non_existent(path: &Path) -> crate::Result<()> { Ok(()) } +pub fn strip_cur_dir(source_path: &Path) -> PathBuf { + source_path + .strip_prefix(Component::CurDir) + .map(|path| path.to_path_buf()) + .unwrap_or_else(|_| source_path.to_path_buf()) +} + /// Changes the process' current directory to the directory that contains the /// file pointed to by `filename` and returns the directory that the process /// was in before this function was called. @@ -46,7 +53,7 @@ pub fn user_wants_to_overwrite(path: &Path, flags: &oof::Flags) -> crate::Result _ => {} } - let path = path.strip_prefix(Component::CurDir).unwrap_or_else(|_| path); + let path = strip_cur_dir(path); Confirmation::new("Do you want to overwrite 'FILE'?", Some("FILE")).ask(Some(&to_utf(path))) } From d533af27d4898a4eb641ba0702f6883bce32fe1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Marcos=20Bezerra?= Date: Wed, 20 Oct 2021 08:30:12 -0300 Subject: [PATCH 34/36] README: add "no runtime dependencies" as a feature Currently only supported by Linux on x86_64 --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index db008da..4ab33f7 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ 2. Automatic format detection. 3. Same syntax, various formats. 4. Encoding and decoding streams, it's fast. +5. No runtime dependencies (for _Linux x86_64_). ## Usage From 10f7462b8b5eb4d79a4460c7d7dad9a5102dd0a9 Mon Sep 17 00:00:00 2001 From: Anton Hermann Date: Thu, 21 Oct 2021 23:16:01 +0200 Subject: [PATCH 35/36] Introduce new type for policy on how to handle questions --- src/archive/tar.rs | 6 +++--- src/archive/zip.rs | 6 +++--- src/cli.rs | 9 +++++---- src/commands.rs | 24 ++++++++++++------------ src/utils.rs | 21 ++++++++++++++++----- tests/compress_and_decompress.rs | 4 ++-- tests/utils.rs | 6 +++--- 7 files changed, 44 insertions(+), 32 deletions(-) diff --git a/src/archive/tar.rs b/src/archive/tar.rs index 96f93e6..bb52d7e 100644 --- a/src/archive/tar.rs +++ b/src/archive/tar.rs @@ -11,13 +11,13 @@ use walkdir::WalkDir; use crate::{ info, - utils::{self, Bytes}, + utils::{self, Bytes, QuestionPolicy}, }; pub fn unpack_archive( reader: Box, output_folder: &Path, - skip_questions_positively: Option, + question_policy: QuestionPolicy, ) -> crate::Result> { let mut archive = tar::Archive::new(reader); @@ -26,7 +26,7 @@ pub fn unpack_archive( let mut file = file?; let file_path = output_folder.join(file.path()?); - if file_path.exists() && !utils::user_wants_to_overwrite(&file_path, skip_questions_positively)? { + if file_path.exists() && !utils::user_wants_to_overwrite(&file_path, question_policy)? { continue; } diff --git a/src/archive/zip.rs b/src/archive/zip.rs index db35727..87d1a17 100644 --- a/src/archive/zip.rs +++ b/src/archive/zip.rs @@ -11,7 +11,7 @@ use zip::{self, read::ZipFile, ZipArchive}; use crate::{ info, - utils::{self, dir_is_empty,strip_cur_dir, Bytes}, + utils::{self, dir_is_empty, strip_cur_dir, Bytes, QuestionPolicy}, }; use self::utf8::get_invalid_utf8_paths; @@ -20,7 +20,7 @@ use self::utf8::get_invalid_utf8_paths; pub fn unpack_archive( mut archive: ZipArchive, into: &Path, - skip_questions_positively: Option, + question_policy: QuestionPolicy, ) -> crate::Result> where R: Read + Seek, @@ -34,7 +34,7 @@ where }; let file_path = into.join(file_path); - if file_path.exists() && !utils::user_wants_to_overwrite(&file_path, skip_questions_positively)? { + if file_path.exists() && !utils::user_wants_to_overwrite(&file_path, question_policy)? { continue; } diff --git a/src/cli.rs b/src/cli.rs index 7a13aab..1ec281b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -7,6 +7,7 @@ use std::{ use clap::{Parser, ValueHint}; +pub use crate::utils::QuestionPolicy; use crate::Error; #[derive(Parser, Debug)] @@ -53,18 +54,18 @@ pub enum Subcommand { impl Opts { /// A helper method that calls `clap::Parser::parse` and then translates relative paths to absolute. /// Also determines if the user wants to skip questions or not - pub fn parse_args() -> crate::Result<(Self, Option)> { + pub fn parse_args() -> crate::Result<(Self, QuestionPolicy)> { let mut opts: Self = Self::parse(); let (Subcommand::Compress { files, .. } | Subcommand::Decompress { files, .. }) = &mut opts.cmd; *files = canonicalize_files(files)?; let skip_questions_positively = if opts.yes { - Some(true) + QuestionPolicy::AlwaysYes } else if opts.no { - Some(false) + QuestionPolicy::AlwaysNo } else { - None + QuestionPolicy::Ask }; Ok((opts, skip_questions_positively)) diff --git a/src/commands.rs b/src/commands.rs index 6d41347..2543c67 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -21,7 +21,7 @@ use crate::{ info, utils::nice_directory_display, utils::to_utf, - utils::{self, dir_is_empty}, + utils::{self, dir_is_empty, QuestionPolicy}, Error, }; @@ -38,7 +38,7 @@ fn represents_several_files(files: &[PathBuf]) -> bool { files.iter().any(is_non_empty_dir) || files.len() > 1 } -pub fn run(args: Opts, skip_questions_positively: Option) -> crate::Result<()> { +pub fn run(args: Opts, question_policy: QuestionPolicy) -> crate::Result<()> { match args.cmd { Subcommand::Compress { files, output: output_path } => { // Formats from path extension, like "file.tar.gz.xz" -> vec![Tar, Gzip, Lzma] @@ -94,7 +94,7 @@ pub fn run(args: Opts, skip_questions_positively: Option) -> crate::Result return Err(Error::with_reason(reason)); } - if output_path.exists() && !utils::user_wants_to_overwrite(&output_path, skip_questions_positively)? { + if output_path.exists() && !utils::user_wants_to_overwrite(&output_path, question_policy)? { // User does not want to overwrite this file return Ok(()); } @@ -176,7 +176,7 @@ pub fn run(args: Opts, skip_questions_positively: Option) -> crate::Result let output_folder = output_folder.as_ref().map(|path| path.as_ref()); for ((input_path, formats), file_name) in files.iter().zip(formats).zip(output_paths) { - decompress_file(input_path, formats, output_folder, file_name, skip_questions_positively)?; + decompress_file(input_path, formats, output_folder, file_name, question_policy)?; } } } @@ -289,7 +289,7 @@ fn decompress_file( formats: Vec, output_folder: Option<&Path>, file_name: &Path, - skip_questions_positively: Option, + question_policy: QuestionPolicy, ) -> crate::Result<()> { // TODO: improve error message let reader = fs::File::open(&input_file_path)?; @@ -311,7 +311,7 @@ fn decompress_file( if let [Zip] = *formats.as_slice() { utils::create_dir_if_non_existent(output_folder)?; let zip_archive = zip::ZipArchive::new(reader)?; - let _files = crate::archive::zip::unpack_archive(zip_archive, output_folder, skip_questions_positively)?; + let _files = crate::archive::zip::unpack_archive(zip_archive, output_folder, question_policy)?; info!("Successfully decompressed archive in {}.", nice_directory_display(output_folder)); return Ok(()); } @@ -349,27 +349,27 @@ fn decompress_file( info!("Successfully decompressed archive in {}.", nice_directory_display(output_path)); } Tar => { - let _ = crate::archive::tar::unpack_archive(reader, output_folder, skip_questions_positively)?; + let _ = crate::archive::tar::unpack_archive(reader, output_folder, question_policy)?; info!("Successfully decompressed archive in {}.", nice_directory_display(output_folder)); } Tgz => { let reader = chain_reader_decoder(&Gzip, reader)?; - let _ = crate::archive::tar::unpack_archive(reader, output_folder, skip_questions_positively)?; + let _ = crate::archive::tar::unpack_archive(reader, output_folder, question_policy)?; info!("Successfully decompressed archive in {}.", nice_directory_display(output_folder)); } Tbz => { let reader = chain_reader_decoder(&Bzip, reader)?; - let _ = crate::archive::tar::unpack_archive(reader, output_folder, skip_questions_positively)?; + let _ = crate::archive::tar::unpack_archive(reader, output_folder, question_policy)?; info!("Successfully decompressed archive in {}.", nice_directory_display(output_folder)); } Tlzma => { let reader = chain_reader_decoder(&Lzma, reader)?; - let _ = crate::archive::tar::unpack_archive(reader, output_folder, skip_questions_positively)?; + let _ = crate::archive::tar::unpack_archive(reader, output_folder, question_policy)?; info!("Successfully decompressed archive in {}.", nice_directory_display(output_folder)); } Tzst => { let reader = chain_reader_decoder(&Zstd, reader)?; - let _ = crate::archive::tar::unpack_archive(reader, output_folder, skip_questions_positively)?; + let _ = crate::archive::tar::unpack_archive(reader, output_folder, question_policy)?; info!("Successfully decompressed archive in {}.", nice_directory_display(output_folder)); } Zip => { @@ -384,7 +384,7 @@ fn decompress_file( io::copy(&mut reader, &mut vec)?; let zip_archive = zip::ZipArchive::new(io::Cursor::new(vec))?; - let _ = crate::archive::zip::unpack_archive(zip_archive, output_folder, skip_questions_positively)?; + let _ = crate::archive::zip::unpack_archive(zip_archive, output_folder, question_policy)?; info!("Successfully decompressed archive in {}.", nice_directory_display(output_folder)); } diff --git a/src/utils.rs b/src/utils.rs index 4e69ab0..7c3a0e7 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -43,11 +43,11 @@ pub fn cd_into_same_dir_as(filename: &Path) -> crate::Result { Ok(previous_location) } -pub fn user_wants_to_overwrite(path: &Path, skip_questions_positively: Option) -> crate::Result { - match skip_questions_positively { - Some(true) => Ok(true), - Some(false) => Ok(false), - None => { +pub fn user_wants_to_overwrite(path: &Path, question_policy: QuestionPolicy) -> crate::Result { + match question_policy { + QuestionPolicy::AlwaysYes => Ok(true), + QuestionPolicy::AlwaysNo => Ok(false), + QuestionPolicy::Ask => { let path = to_utf(strip_cur_dir(path)); let path = Some(path.as_str()); let placeholder = Some("FILE"); @@ -126,6 +126,17 @@ impl std::fmt::Display for Bytes { } } +#[derive(Debug, PartialEq, Clone, Copy)] +/// How overwrite questions should be handled +pub enum QuestionPolicy { + /// Ask ever time + Ask, + /// Skip overwrite questions positively + AlwaysYes, + /// Skip overwrite questions negatively + AlwaysNo, +} + #[cfg(test)] mod tests { use super::*; diff --git a/tests/compress_and_decompress.rs b/tests/compress_and_decompress.rs index 77d929d..45518d0 100644 --- a/tests/compress_and_decompress.rs +++ b/tests/compress_and_decompress.rs @@ -8,7 +8,7 @@ use std::{ }; use ouch::{ - cli::{Opts, Subcommand}, + cli::{Opts, QuestionPolicy, Subcommand}, commands::run, }; use rand::{rngs::SmallRng, RngCore, SeedableRng}; @@ -183,7 +183,7 @@ fn extract_files(archive_path: &Path) -> Vec { output: Some(extraction_output_folder.clone()), }, }; - run(command, None).expect("Failed to extract"); + run(command, QuestionPolicy::Ask).expect("Failed to extract"); fs::read_dir(extraction_output_folder).unwrap().map(Result::unwrap).map(|entry| entry.path()).collect() } diff --git a/tests/utils.rs b/tests/utils.rs index 10cc749..e55aac2 100644 --- a/tests/utils.rs +++ b/tests/utils.rs @@ -8,7 +8,7 @@ use std::{ }; use ouch::{ - cli::{Opts, Subcommand}, + cli::{Opts, QuestionPolicy, Subcommand}, commands::run, }; @@ -30,7 +30,7 @@ pub fn compress_files(at: &Path, paths_to_compress: &[PathBuf], format: &str) -> no: false, cmd: Subcommand::Compress { files: paths_to_compress.to_vec(), output: archive_path.clone() }, }; - run(command, None).expect("Failed to compress test dummy files"); + run(command, QuestionPolicy::Ask).expect("Failed to compress test dummy files"); archive_path } @@ -55,7 +55,7 @@ pub fn extract_files(archive_path: &Path) -> Vec { output: Some(extraction_output_folder.clone()), }, }; - run(command, None).expect("Failed to extract"); + run(command, QuestionPolicy::Ask).expect("Failed to extract"); fs::read_dir(extraction_output_folder).unwrap().map(Result::unwrap).map(|entry| entry.path()).collect() } From 01b6dc89deddbf638e587c870c453bca6b6bed1f Mon Sep 17 00:00:00 2001 From: Santo Cariotti Date: Wed, 27 Oct 2021 15:58:31 +0200 Subject: [PATCH 36/36] Check the format with Github Action (#126) * Add cargo fmt to the CI Use a job to check the format of the sourcecode with `cargo fmt` command. The new step does not change any code, but returns an error when `check` does not pass with 0 issues. --- .github/workflows/build.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4f6592f..aa0f697 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -274,3 +274,26 @@ jobs: # with: # name: 'ouch-x86_64-pc-windows-gnu' # path: target\x86_64-pc-windows-gnu\release\ouch.exe + + fmt: + name: Check sourcecode format + runs-on: ubuntu-latest + + steps: + - name: Checkout sources + uses: actions/checkout@v2 + + - name: Install toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly + target: x86_64-unknown-linux-musl + components: rustfmt + override: true + + - name: Check format with cargo fmt + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check