diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4f6592f33..aa0f69702 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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..67be18c70 --- /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. diff --git a/Cargo.lock b/Cargo.lock index cf9225076..c098ebdb9 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", ] @@ -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" @@ -94,9 +125,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", @@ -134,6 +165,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" @@ -143,6 +189,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" @@ -195,6 +251,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" @@ -205,19 +267,34 @@ dependencies = [ "autocfg", ] +[[package]] +name = "once_cell" +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", "fs-err", "infer", - "lazy_static", "libc", + "once_cell", "rand", - "strsim", "tar", "tempfile", "walkdir", @@ -228,30 +305,54 @@ 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 = "c3ca011bd0129ff4ae15cd04c4eef202cadf6c51c21e47aba319b4e0501db741" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" +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.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", ] @@ -298,9 +399,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", ] @@ -331,9 +432,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", @@ -365,26 +466,65 @@ 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.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", "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" @@ -403,6 +543,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 4d8320e25..248f33a4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,11 +13,11 @@ 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" fs-err = "2.6.0" -lazy_static = "1.4.0" +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/README.md b/README.md index 4c70faef2..4ab33f73e 100644 --- a/README.md +++ b/README.md @@ -2,124 +2,101 @@ [![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. +5. No runtime dependencies (for _Linux x86_64_). + ## 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 - -# Compress folder and video into 'videos.tar.gz' -ouch compress videos/ meme.mp4 videos.tar.gz +# Short alternative +ouch c file.txt file.zip -# 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 +[![Packaging status](https://repology.org/badge/vertical-allrepos/ouch.svg)](https://repology.org/project/ouch/versions) + ### 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 | .zip | .bz, .bz2 | .gz | .xz, .lz, .lzma | .zst | +| Format | .tar | .zip | .bz, .bz2 | .gz | .xz, .lz, .lzma | .zst | |:-------------:|:----:|:----:|:---------:| --- |:---------------:| --- | -| Decompression | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Compression | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Supported | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | + +And the aliases: `tgz`, `tbz`, `tbz2`, `txz`, `tlz`, `tlzma`, `tzst`. + +Formats can be chained (`ouch` keeps it _fast_): -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... +- `.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! diff --git a/rust-toolchain b/rust-toolchain new file mode 100644 index 000000000..bf867e0ae --- /dev/null +++ b/rust-toolchain @@ -0,0 +1 @@ +nightly diff --git a/rustfmt.toml b/rustfmt.toml index 7bba11e2a..2ac63bb9b 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/archive/tar.rs b/src/archive/tar.rs index e92c31dbc..6b2327822 100644 --- a/src/archive/tar.rs +++ b/src/archive/tar.rs @@ -12,11 +12,15 @@ use walkdir::WalkDir; use crate::{ error::FinalError, - info, oof, - utils::{self, Bytes}, + info, + utils::{self, Bytes, QuestionPolicy}, }; -pub fn unpack_archive(reader: Box, output_folder: &Path, flags: &oof::Flags) -> crate::Result> { +pub fn unpack_archive( + reader: Box, + output_folder: &Path, + question_policy: QuestionPolicy, +) -> crate::Result> { let mut archive = tar::Archive::new(reader); let mut files_unpacked = vec![]; @@ -24,7 +28,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, question_policy)? { continue; } diff --git a/src/archive/zip.rs b/src/archive/zip.rs index 1b07c20dc..4ef9a8500 100644 --- a/src/archive/zip.rs +++ b/src/archive/zip.rs @@ -12,14 +12,18 @@ use walkdir::WalkDir; use zip::{self, read::ZipFile, ZipArchive}; use crate::{ - info, oof, - utils::{self, dir_is_empty, Bytes}, + info, + utils::{self, dir_is_empty, strip_cur_dir, Bytes, QuestionPolicy}, }; 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, + question_policy: QuestionPolicy, +) -> crate::Result> where R: Read + Seek, { @@ -32,7 +36,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, question_policy)? { continue; } @@ -49,6 +53,7 @@ where fs::create_dir_all(&path)?; } } + let file_path = strip_cur_dir(file_path.as_path()); info!("{:?} extracted. ({})", file_path.display(), Bytes::new(file.size())); diff --git a/src/cli.rs b/src/cli.rs index 8d93c0118..e0b8441ca 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,77 +1,76 @@ -//! 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 clap::{Parser, ValueHint}; use fs_err as fs; -use strsim::normalized_damerau_levenshtein; +pub use crate::utils::QuestionPolicy; +use crate::Error; -use crate::{arg_flag, flag, oof, Error}; +#[derive(Parser, Debug)] +#[clap(version, about)] +pub struct Opts { + /// Skip overwrite questions positively. + #[clap(short, long, conflicts_with = "no")] + pub yes: bool, -#[derive(PartialEq, Eq, Debug)] -pub enum Command { - /// Files to be compressed + /// 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)?; - - // 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)?; - } - _ => {} - } - - if parsed_args.flags.is_present("yes") && parsed_args.flags.is_present("no") { - todo!("conflicting flags, better error message."); - } +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, QuestionPolicy)> { + let mut opts: Self = Self::parse(); - Ok(parsed_args) -} + let (Subcommand::Compress { files, .. } | Subcommand::Decompress { files, .. }) = &mut opts.cmd; + *files = canonicalize_files(files)?; -#[derive(Debug)] -pub struct ParsedArgs { - pub command: Command, - pub flags: oof::Flags, -} + let skip_questions_positively = if opts.yes { + QuestionPolicy::AlwaysYes + } else if opts.no { + QuestionPolicy::AlwaysNo + } else { + QuestionPolicy::Ask + }; -/// 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; + Ok((opts, skip_questions_positively)) } - - 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 { @@ -90,120 +89,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 b97338c88..6d99a7715 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -12,15 +12,16 @@ use utils::colors; use crate::{ archive, - cli::Command, + cli::{Opts, Subcommand}, error::FinalError, extension::{ self, CompressionFormat::{self, *}, }, - info, oof, + info, + utils::nice_directory_display, utils::to_utf, - utils::{self, dir_is_empty}, + utils::{self, dir_is_empty, QuestionPolicy}, Error, }; @@ -29,7 +30,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() }; @@ -37,11 +38,11 @@ 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, 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] - 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))) @@ -50,14 +51,13 @@ 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))) - .into_owned(); + .hint(format!(" ouch compress ... {}.zip", to_utf(&output_path))); return Err(Error::with_reason(reason)); } 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 @@ -79,40 +79,64 @@ 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)) - .into_owned(); + .hint(format!(" To : {}", suggested_output_path)); 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))) - .into_owned(); + .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))); 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, question_policy)? { // User does not want to overwrite this file return Ok(()); } let output_file = fs::File::create(&output_path)?; - let compress_result = compress_files(files, formats, output_file, flags); + + 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]); + + // 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) + { + // 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()), + to_utf(&output_path) + ); + 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); // If any error occurred, delete incomplete file if compress_result.is_err() { // 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)); @@ -120,7 +144,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![]; @@ -152,32 +176,34 @@ 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, question_policy)?; } } - 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 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, + if let [Tar | Tgz | Zip] = *formats.as_slice() { + 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,8 +239,28 @@ fn compress_files( let mut writer = archive::tar::build_archive_from_paths(&files, writer)?; writer.flush()?; } + Tgz => { + 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()); + 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!( @@ -243,7 +289,7 @@ fn decompress_file( formats: Vec, output_folder: Option<&Path>, file_name: &Path, - flags: &oof::Flags, + question_policy: QuestionPolicy, ) -> crate::Result<()> { // TODO: improve error message let reader = fs::File::open(&input_file_path)?; @@ -265,8 +311,8 @@ 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)?; - info!("Successfully uncompressed archive in '{}'.", to_utf(output_folder)); + 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(()); } @@ -290,6 +336,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)?; @@ -298,16 +346,33 @@ 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 {}.", nice_directory_display(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)); + 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, 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, 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, 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, question_policy)?; + info!("Successfully decompressed archive in {}.", nice_directory_display(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!( @@ -319,9 +384,9 @@ 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, question_policy)?; - info!("Successfully uncompressed archive in '{}'.", to_utf(output_folder)); + info!("Successfully decompressed archive in {}.", nice_directory_display(output_folder)); } } diff --git a/src/dialogs.rs b/src/dialogs.rs index 95db53fc7..4f4d7b24e 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 090c2ec77..4773970f9 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 { error_title: String }, UnsupportedZipArchive(&'static str), InternalError, - OofError(oof::OofError), CompressingRootFolder, MissingArgumentsForCompression, MissingArgumentsForDecompression, @@ -45,11 +44,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 +56,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) } } @@ -70,12 +69,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 } @@ -89,70 +88,53 @@ 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") - .into_owned(); - - 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.") - .into_owned(); - - 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`") - .into_owned(); - - 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`") - .into_owned(); - - 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())) - .into_owned(); - - error + .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())) - .into_owned(), + 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!(), @@ -202,12 +184,6 @@ impl From for Error { } } -impl From for Error { - fn from(err: oof::OofError) -> Self { - Self::OofError(err) - } -} - impl From for Error { fn from(err: FinalError) -> Self { Self::Custom { reason: err } diff --git a/src/extension.rs b/src/extension.rs index 5f33b5846..b1828f063 100644 --- a/src/extension.rs +++ b/src/extension.rs @@ -1,18 +1,22 @@ //! Our representation of all the supported compression formats. -use std::{fmt, path::Path}; +use std::{ffi::OsStr, fmt, path::Path}; 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) - 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 { @@ -26,6 +30,10 @@ impl fmt::Display for CompressionFormat { Zstd => ".zst", Lzma => ".lz", Tar => ".tar", + Tgz => ".tgz", + Tbz => ".tbz", + Tlzma => ".tlz", + Tzst => ".tzst", Zip => ".zip", } ) @@ -44,18 +52,20 @@ 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, + "tbz" | "tbz2" => Tbz, + "txz" | "tlz" | "tlzma" => Tlzma, + "tzst" => Tzst, + "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/src/lib.rs b/src/lib.rs index b6fd3b73f..9a1a795a6 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; @@ -19,60 +18,5 @@ 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: &'static 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::*; - - 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/macros.rs b/src/macros.rs index bed8ba91d..76877fad7 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -1,24 +1,13 @@ -use crate::NO_COLOR_IS_SET; - #[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)*); }; } 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/main.rs b/src/main.rs index 57a5c6055..9fc88ec8b 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 6dd478352..000000000 --- a/src/oof/error.rs +++ /dev/null @@ -1,51 +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 231ec5cb9..000000000 --- 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 788113b10..000000000 --- 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 547981899..000000000 --- a/src/oof/util.rs +++ /dev/null @@ -1,33 +0,0 @@ -/// 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() -} - -#[cfg(test)] -mod tests { - use super::trim_double_hyphen; - use super::trim_single_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"); - } - - 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 6c2b21481..3c7749594 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,18 +1,19 @@ use std::{ cmp, env, ffi::OsStr, + path::Component, path::{Path, PathBuf}, }; use fs_err as fs; -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 { let is_empty = |mut rd: std::fs::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<()> { @@ -23,6 +24,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. @@ -36,22 +44,17 @@ 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.") +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"); + Confirmation::new("Do you want to overwrite 'FILE'?", placeholder).ask(path) } - (true, _) => return Ok(true), - (_, true) => return Ok(false), - _ => {} } - - 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)) } pub fn to_utf(os_str: impl AsRef) -> String { @@ -59,58 +62,46 @@ 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, } /// 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" - } - 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 { - "" + use once_cell::sync::Lazy; + + 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 *DISABLE_COLORED_TEXT { "" } else { $value }); + #[cfg(not(target_family = "unix"))] + pub static $name: &&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 { @@ -136,6 +127,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 3879e2b7c..f78a5aaf2 100644 --- a/tests/compress_and_decompress.rs +++ b/tests/compress_and_decompress.rs @@ -9,7 +9,10 @@ use std::{ use fs_err as fs; -use ouch::{cli::Command, commands::run, oof}; +use ouch::{ + cli::{Opts, QuestionPolicy, Subcommand}, + commands::run, +}; use rand::{rngs::SmallRng, RngCore, SeedableRng}; use tempfile::NamedTempFile; use utils::*; @@ -29,12 +32,22 @@ 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", "tbz", "tbz2", "txz", "tlz", "tlzma", "tzst", "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", + "application/x-xz", + "application/x-xz", + "application/zstd", "application/x-bzip2", "application/x-bzip2", "application/x-xz", @@ -69,6 +82,13 @@ 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("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"); @@ -103,9 +123,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); @@ -157,11 +177,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, 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/compress_empty_dir.rs b/tests/compress_empty_dir.rs index 95f334899..57bfe35b3 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 60d1ea344..0d0fdd068 100644 --- a/tests/utils.rs +++ b/tests/utils.rs @@ -6,7 +6,10 @@ use std::path::{Path, PathBuf}; use fs_err as fs; -use ouch::{cli::Command, commands::run, oof}; +use ouch::{ + cli::{Opts, QuestionPolicy, Subcommand}, + commands::run, +}; pub fn create_empty_dir(at: &Path, filename: &str) -> PathBuf { let dirname = Path::new(filename); @@ -21,8 +24,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.to_path_buf() }; - 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, QuestionPolicy::Ask).expect("Failed to compress test dummy files"); archive_path } @@ -39,11 +46,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, QuestionPolicy::Ask).expect("Failed to extract"); fs::read_dir(extraction_output_folder).unwrap().map(Result::unwrap).map(|entry| entry.path()).collect() }