Merge branch 'master' into issue-56

This commit is contained in:
Gabriel Simonetto 2021-10-30 11:31:09 -03:00
commit 704a4efdd7
25 changed files with 616 additions and 1119 deletions

View File

@ -274,3 +274,26 @@ jobs:
# with: # with:
# name: 'ouch-x86_64-pc-windows-gnu' # name: 'ouch-x86_64-pc-windows-gnu'
# path: target\x86_64-pc-windows-gnu\release\ouch.exe # 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

15
CONTRIBUTING.md Normal file
View File

@ -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.

196
Cargo.lock generated
View File

@ -27,9 +27,9 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.2.1" version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
@ -60,9 +60,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.69" version = "1.0.71"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd"
dependencies = [ dependencies = [
"jobserver", "jobserver",
] ]
@ -83,6 +83,37 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 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]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.2.1" version = "1.2.1"
@ -94,9 +125,9 @@ dependencies = [
[[package]] [[package]]
name = "filetime" name = "filetime"
version = "0.2.14" version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8" checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
@ -134,6 +165,21 @@ dependencies = [
"wasi", "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]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.1.19" version = "0.1.19"
@ -143,6 +189,16 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "indexmap"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
dependencies = [
"autocfg",
"hashbrown",
]
[[package]] [[package]]
name = "infer" name = "infer"
version = "0.5.0" version = "0.5.0"
@ -195,6 +251,12 @@ dependencies = [
"pkg-config", "pkg-config",
] ]
[[package]]
name = "memchr"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.4.4" version = "0.4.4"
@ -205,19 +267,34 @@ dependencies = [
"autocfg", "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]] [[package]]
name = "ouch" name = "ouch"
version = "0.2.0" version = "0.2.0"
dependencies = [ dependencies = [
"atty", "atty",
"bzip2", "bzip2",
"clap",
"flate2", "flate2",
"fs-err", "fs-err",
"infer", "infer",
"lazy_static",
"libc", "libc",
"once_cell",
"rand", "rand",
"strsim",
"tar", "tar",
"tempfile", "tempfile",
"walkdir", "walkdir",
@ -228,30 +305,54 @@ dependencies = [
[[package]] [[package]]
name = "pkg-config" name = "pkg-config"
version = "0.3.19" version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" checksum = "7c9b1041b4387893b91ee6746cddfc28516aff326a3519fb2adf820932c5e6cb"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.10" version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" 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]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.28" version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612" checksum = "edc3358ebc67bc8b7fa0c007f945b0b18226f78437d61bec735a9eb96b61ee70"
dependencies = [ dependencies = [
"unicode-xid", "unicode-xid",
] ]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.9" version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@ -298,9 +399,9 @@ dependencies = [
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.2.9" version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee" checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
dependencies = [ dependencies = [
"bitflags", "bitflags",
] ]
@ -331,9 +432,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.74" version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c" checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -366,25 +467,64 @@ dependencies = [
] ]
[[package]] [[package]]
name = "thiserror" name = "termcolor"
version = "1.0.26" version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2" 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.26" version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745" checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "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]] [[package]]
name = "unicode-xid" name = "unicode-xid"
version = "0.2.2" version = "0.2.2"
@ -403,6 +543,12 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]] [[package]]
name = "walkdir" name = "walkdir"
version = "2.3.2" version = "2.3.2"

View File

@ -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 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
clap = "=3.0.0-beta.5" # Keep it pinned while in beta!
atty = "0.2.14" atty = "0.2.14"
fs-err = "2.6.0" fs-err = "2.6.0"
lazy_static = "1.4.0" once_cell = "1.8.0"
walkdir = "2.3.2" walkdir = "2.3.2"
strsim = "0.10.0"
bzip2 = "0.4.3" bzip2 = "0.4.3"
libc = "0.2.103" libc = "0.2.103"
tar = "0.4.37" tar = "0.4.37"

113
README.md
View File

@ -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) [![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_image](https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR5ilNDTFZZ-Vy_ctm2YyAe8Yk0UT7lB2hIhg&usqp=CAU) --> `ouch` stands for **Obvious Unified Compression Helper**, it's a CLI tool to compress and decompress files.
`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.
<!-- TODO -->
<!-- - [Listing files](#Listing-the-elements-of-an-archive) -->
- [Features](#features)
- [Usage](#usage) - [Usage](#usage)
- [Decompressing](#decompressing)
- [Compressing](#compressing)
- [Installation](#installation) - [Installation](#installation)
- [Latest binary](#downloading-the-latest-binary)
- [Compiling from source](#installing-from-source-code)
- [Supported Formats](#supported-formats) - [Supported Formats](#supported-formats)
- [Contributing](#contributing) - [Contributing](#contributing)
## Features
1. Easy to use.
2. Automatic format detection.
3. Same syntax, various formats.
4. Encoding and decoding streams, it's fast. <!-- We should post benchmarks in our wiki and link them here -->
5. No runtime dependencies (for _Linux x86_64_).
## Usage ## Usage
### Decompressing ### Decompressing
Run `ouch` and pass compressed files as arguments. Use the `decompress` subcommand and pass the files.
```sh ```sh
# Decompress 'a.zip' # Decompress one
ouch decompress a.zip ouch decompress a.zip
# Also works with the short version # Decompress multiple
ouch d a.zip ouch decompress a.zip b.tar.gz c.tar
# Decompress multiple files # Short alternative
ouch decompress a.zip b.tar.gz 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 ```sh
# Create 'pictures' folder and decompress inside of it # Decompress 'summer_vacation.zip' inside of new folder 'pictures'
ouch decompress a.zip -o pictures ouch decompress summer_vacation.zip -o pictures
``` ```
### Compressing ### Compressing
Use the `compress` subcommand. Use the `compress` subcommand, pass the files and the **output file** at the end.
Accepts multiple files and folders, the **last** argument shall be the **output file**.
```sh ```sh
# Compress four files into 'archive.zip' # Compress four files/folders
ouch compress 1 2 3 4 archive.zip ouch compress 1 2 3 4 archive.zip
# Also works with the short version # Short alternative
ouch c 1 2 3 4 archive.zip ouch c file.txt file.zip
# Compress folder and video into 'videos.tar.gz' # Compress everything in the current folder again and again
ouch compress videos/ meme.mp4 videos.tar.gz ouch compress * everything.tar.gz.xz.bz.zst.gz.gz.gz.gz.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
``` ```
`ouch` checks for the extensions of the **output file** to decide which formats should be used. `ouch` checks for the extensions of the **output file** to decide which formats should be used.
<!-- ### Listing the elements of an archive
* **Upcoming feature**
```
# Shows the files and folders contained in videos.tar.xz
ouch list videos.tar.xz
``` -->
## Installation ## Installation
[![Packaging status](https://repology.org/badge/vertical-allrepos/ouch.svg)](https://repology.org/project/ouch/versions)
### Downloading the latest binary ### 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 | Method | Command |
curl -s https://raw.githubusercontent.com/ouch-org/ouch/master/install.sh | 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` |
Or with `wget`.
```sh The script will download the [latest binary](https://github.com/ouch-org/ouch/releases) and copy it to `/usr/bin`.
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`.
### Installing from source code ### 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 ## Supported formats
| | .tar | .zip | .bz, .bz2 | .gz | .xz, .lz, .lzma | .zst | | Format | .tar | .zip | .bz, .bz2 | .gz | .xz, .lz, .lzma | .zst |
|:-------------:|:----:|:----:|:---------:| --- |:---------------:| --- | |:-------------:|:----:|:----:|:---------:| --- |:---------------:| --- |
| Decompression | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | Supported | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Compression | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
Note that formats can be chained: And the aliases: `tgz`, `tbz`, `tbz2`, `txz`, `tlz`, `tlzma`, `tzst`.
- `.tar.gz`
- `.tar.xz` Formats can be chained (`ouch` keeps it _fast_):
- `.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.bz.zst`
- `.gz.xz` - `.tar.gz.xz.bz.zst`
- etc... - `.tar.gz.gz.gz.gz.xz.xz.xz.xz.bz.bz.bz.bz.zst.zst.zst.zst`
## Contributing ## Contributing
`ouch` is 100% made out of voluntary work, any small contribution is welcome! `ouch` is 100% made out of voluntary work, any small contribution is welcome!
- Open an issue. - Open an issue.
- Open a pr. - Open a pull request.
- Share it to a friend. - Share it to a friend!

1
rust-toolchain Normal file
View File

@ -0,0 +1 @@
nightly

View File

@ -6,3 +6,5 @@ reorder_imports = true
reorder_modules = true reorder_modules = true
use_try_shorthand = true use_try_shorthand = true
use_small_heuristics = "Max" use_small_heuristics = "Max"
unstable_features = true
force_multiline_blocks = true

View File

@ -12,11 +12,15 @@ use walkdir::WalkDir;
use crate::{ use crate::{
error::FinalError, error::FinalError,
info, oof, info,
utils::{self, Bytes}, utils::{self, Bytes, QuestionPolicy},
}; };
pub fn unpack_archive(reader: Box<dyn Read>, output_folder: &Path, flags: &oof::Flags) -> crate::Result<Vec<PathBuf>> { pub fn unpack_archive(
reader: Box<dyn Read>,
output_folder: &Path,
question_policy: QuestionPolicy,
) -> crate::Result<Vec<PathBuf>> {
let mut archive = tar::Archive::new(reader); let mut archive = tar::Archive::new(reader);
let mut files_unpacked = vec![]; let mut files_unpacked = vec![];
@ -24,7 +28,7 @@ pub fn unpack_archive(reader: Box<dyn Read>, output_folder: &Path, flags: &oof::
let mut file = file?; let mut file = file?;
let file_path = output_folder.join(file.path()?); 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; continue;
} }

View File

@ -12,14 +12,18 @@ use walkdir::WalkDir;
use zip::{self, read::ZipFile, ZipArchive}; use zip::{self, read::ZipFile, ZipArchive};
use crate::{ use crate::{
info, oof, info,
utils::{self, dir_is_empty, Bytes}, utils::{self, dir_is_empty, strip_cur_dir, Bytes, QuestionPolicy},
}; };
use self::utf8::get_invalid_utf8_paths; use self::utf8::get_invalid_utf8_paths;
/// Unpacks the archive given by `archive` into the folder given by `into`. /// Unpacks the archive given by `archive` into the folder given by `into`.
pub fn unpack_archive<R>(mut archive: ZipArchive<R>, into: &Path, flags: &oof::Flags) -> crate::Result<Vec<PathBuf>> pub fn unpack_archive<R>(
mut archive: ZipArchive<R>,
into: &Path,
question_policy: QuestionPolicy,
) -> crate::Result<Vec<PathBuf>>
where where
R: Read + Seek, R: Read + Seek,
{ {
@ -32,7 +36,7 @@ where
}; };
let file_path = into.join(file_path); 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; continue;
} }
@ -49,6 +53,7 @@ where
fs::create_dir_all(&path)?; fs::create_dir_all(&path)?;
} }
} }
let file_path = strip_cur_dir(file_path.as_path());
info!("{:?} extracted. ({})", file_path.display(), Bytes::new(file.size())); info!("{:?} extracted. ({})", file_path.display(), Bytes::new(file.size()));

View File

@ -1,77 +1,76 @@
//! CLI arg parser configuration, command detection and input treatment. //! CLI arg parser configuration, command detection and input treatment.
//!
//! NOTE: the argparser implementation itself is not in this file.
use std::{ use std::{
env,
ffi::OsString,
path::{Path, PathBuf}, path::{Path, PathBuf},
vec::Vec, vec::Vec,
}; };
use clap::{Parser, ValueHint};
use fs_err as fs; 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)] /// Skip overwrite questions negatively.
pub enum Command { #[clap(short, long)]
/// Files to be compressed pub no: bool,
#[clap(subcommand)]
pub cmd: Subcommand,
}
#[derive(Parser, PartialEq, Eq, Debug)]
pub enum Subcommand {
/// Compress files. Alias: c
#[clap(alias = "c")]
Compress { Compress {
/// Files to be compressed
#[clap(required = true, min_values = 1)]
files: Vec<PathBuf>, files: Vec<PathBuf>,
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 { Decompress {
/// Files to be decompressed
#[clap(required = true, min_values = 1)]
files: Vec<PathBuf>, files: Vec<PathBuf>,
output_folder: Option<PathBuf>,
/// Decompress files in a directory other than the current
#[clap(short, long, value_hint = ValueHint::DirPath)]
output: Option<PathBuf>,
}, },
ShowHelp,
ShowVersion,
} }
/// Calls parse_args_and_flags_from using argv (std::env::args_os) impl Opts {
/// /// A helper method that calls `clap::Parser::parse` and then translates relative paths to absolute.
/// This function is also responsible for treating and checking the command-line input, /// Also determines if the user wants to skip questions or not
/// such as calling [`canonicalize`](std::fs::canonicalize), checking if it the given files exists, etc. pub fn parse_args() -> crate::Result<(Self, QuestionPolicy)> {
pub fn parse_args() -> crate::Result<ParsedArgs> { let mut opts: Self = Self::parse();
// 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 let (Subcommand::Compress { files, .. } | Subcommand::Decompress { files, .. }) = &mut opts.cmd;
match &mut parsed_args.command {
Command::Compress { files, .. } | Command::Decompress { files, .. } => {
*files = canonicalize_files(files)?; *files = canonicalize_files(files)?;
}
_ => {}
}
if parsed_args.flags.is_present("yes") && parsed_args.flags.is_present("no") { let skip_questions_positively = if opts.yes {
todo!("conflicting flags, better error message."); QuestionPolicy::AlwaysYes
} } else if opts.no {
QuestionPolicy::AlwaysNo
} else {
QuestionPolicy::Ask
};
Ok(parsed_args) Ok((opts, skip_questions_positively))
} }
#[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<Path>) -> 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<Path>) -> crate::Result<PathBuf> { fn canonicalize(path: impl AsRef<Path>) -> crate::Result<PathBuf> {
@ -90,120 +89,3 @@ fn canonicalize(path: impl AsRef<Path>) -> crate::Result<PathBuf> {
fn canonicalize_files(files: &[impl AsRef<Path>]) -> crate::Result<Vec<PathBuf>> { fn canonicalize_files(files: &[impl AsRef<Path>]) -> crate::Result<Vec<PathBuf>> {
files.iter().map(canonicalize).collect() files.iter().map(canonicalize).collect()
} }
pub fn parse_args_from(mut args: Vec<OsString>) -> crate::Result<ParsedArgs> {
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<PathBuf> = 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<T: From<OsString>>(text: &str) -> Vec<T> {
let args = text.split_whitespace();
args.map(OsString::from).map(T::from).collect()
}
fn test_cli(args: &str) -> crate::Result<ParsedArgs> {
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(),
}
);
}
}

View File

@ -12,15 +12,16 @@ use utils::colors;
use crate::{ use crate::{
archive, archive,
cli::Command, cli::{Opts, Subcommand},
error::FinalError, error::FinalError,
extension::{ extension::{
self, self,
CompressionFormat::{self, *}, CompressionFormat::{self, *},
}, },
info, oof, info,
utils::nice_directory_display,
utils::to_utf, utils::to_utf,
utils::{self, dir_is_empty}, utils::{self, dir_is_empty, QuestionPolicy},
Error, Error,
}; };
@ -29,7 +30,7 @@ const BUFFER_CAPACITY: usize = 1024 * 64;
fn represents_several_files(files: &[PathBuf]) -> bool { fn represents_several_files(files: &[PathBuf]) -> bool {
let is_non_empty_dir = |path: &PathBuf| { 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() 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 files.iter().any(is_non_empty_dir) || files.len() > 1
} }
pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> { pub fn run(args: Opts, question_policy: QuestionPolicy) -> crate::Result<()> {
match command { match args.cmd {
Command::Compress { files, output_path } => { Subcommand::Compress { files, output: output_path } => {
// Formats from path extension, like "file.tar.gz.xz" -> vec![Tar, Gzip, Lzma] // 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() { if formats.is_empty() {
let reason = FinalError::with_title(format!("Cannot compress to '{}'.", to_utf(&output_path))) 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("")
.hint("Examples:") .hint("Examples:")
.hint(format!(" ouch compress ... {}.tar.gz", to_utf(&output_path))) .hint(format!(" ouch compress ... {}.tar.gz", to_utf(&output_path)))
.hint(format!(" ouch compress ... {}.zip", to_utf(&output_path))) .hint(format!(" ouch compress ... {}.zip", to_utf(&output_path)));
.into_owned();
return Err(Error::with_reason(reason)); return Err(Error::with_reason(reason));
} }
if matches!(&formats[0], Bzip | Gzip | Lzma) && represents_several_files(&files) { 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: // It says:
// Change from file.bz.xz // Change from file.bz.xz
// To file.tar.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.") .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!("Try inserting '.tar' or '.zip' before '{}'.", &formats[0]))
.hint(format!("From: {}", output_path)) .hint(format!("From: {}", output_path))
.hint(format!(" To : {}", suggested_output_path)) .hint(format!(" To : {}", suggested_output_path));
.into_owned();
return Err(Error::with_reason(reason)); 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))) 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!("Found the format '{}' in an incorrect position.", format))
.detail(format!("{} can only be used at the start of the file extension.", 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!("If you wish to compress multiple files, start the extension with '{}'.", format))
.hint(format!("Otherwise, remove {} from '{}'.", format, to_utf(&output_path))) .hint(format!("Otherwise, remove the last '{}' from '{}'.", format, to_utf(&output_path)));
.into_owned();
return Err(Error::with_reason(reason)); 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 // User does not want to overwrite this file
return Ok(()); return Ok(());
} }
let output_file = fs::File::create(&output_path)?; 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 any error occurred, delete incomplete file
if compress_result.is_err() { if compress_result.is_err() {
// Print an extra alert message pointing out that we left a possibly // Print an extra alert message pointing out that we left a possibly
// CORRUPTED FILE at `output_path` // CORRUPTED FILE at `output_path`
if let Err(err) = fs::remove_file(&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!(" Please manually delete '{}'.", to_utf(&output_path));
eprintln!(" Compression failed and we could not 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 { } else {
info!("Successfully compressed '{}'.", to_utf(output_path)); info!("Successfully compressed '{}'.", to_utf(output_path));
@ -120,7 +144,7 @@ pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> {
compress_result?; compress_result?;
} }
Command::Decompress { files, output_folder } => { Subcommand::Decompress { files, output: output_folder } => {
let mut output_paths = vec![]; let mut output_paths = vec![];
let mut formats = 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()); 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) { 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(()) Ok(())
} }
fn compress_files( fn compress_files(files: Vec<PathBuf>, formats: Vec<CompressionFormat>, output_file: fs::File) -> crate::Result<()> {
files: Vec<PathBuf>,
formats: Vec<CompressionFormat>,
output_file: fs::File,
_flags: &oof::Flags,
) -> crate::Result<()> {
let file_writer = BufWriter::with_capacity(BUFFER_CAPACITY, output_file); let file_writer = BufWriter::with_capacity(BUFFER_CAPACITY, output_file);
if formats.len() == 1 { if let [Tar | Tgz | Zip] = *formats.as_slice() {
let build_archive_from_paths = match formats[0] { match formats[0] {
Tar => archive::tar::build_archive_from_paths, Tar => {
Zip => archive::zip::build_archive_from_paths, 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!(), _ => unreachable!(),
}; };
let mut bufwriter = build_archive_from_paths(&files, file_writer)?;
bufwriter.flush()?;
} else { } else {
let mut writer: Box<dyn Write> = Box::new(file_writer); let mut writer: Box<dyn Write> = Box::new(file_writer);
@ -213,8 +239,28 @@ fn compress_files(
let mut writer = archive::tar::build_archive_from_paths(&files, writer)?; let mut writer = archive::tar::build_archive_from_paths(&files, writer)?;
writer.flush()?; 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 => { 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!("\tCompressing .zip entirely in memory.");
eprintln!("\tIf the file is too big, your PC might freeze!"); eprintln!("\tIf the file is too big, your PC might freeze!");
eprintln!( eprintln!(
@ -243,7 +289,7 @@ fn decompress_file(
formats: Vec<extension::CompressionFormat>, formats: Vec<extension::CompressionFormat>,
output_folder: Option<&Path>, output_folder: Option<&Path>,
file_name: &Path, file_name: &Path,
flags: &oof::Flags, question_policy: QuestionPolicy,
) -> crate::Result<()> { ) -> crate::Result<()> {
// TODO: improve error message // TODO: improve error message
let reader = fs::File::open(&input_file_path)?; let reader = fs::File::open(&input_file_path)?;
@ -265,8 +311,8 @@ fn decompress_file(
if let [Zip] = *formats.as_slice() { if let [Zip] = *formats.as_slice() {
utils::create_dir_if_non_existent(output_folder)?; utils::create_dir_if_non_existent(output_folder)?;
let zip_archive = zip::ZipArchive::new(reader)?; 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, question_policy)?;
info!("Successfully uncompressed archive in '{}'.", to_utf(output_folder)); info!("Successfully decompressed archive in {}.", nice_directory_display(output_folder));
return Ok(()); return Ok(());
} }
@ -290,6 +336,8 @@ fn decompress_file(
reader = chain_reader_decoder(format, reader)?; reader = chain_reader_decoder(format, reader)?;
} }
utils::create_dir_if_non_existent(output_folder)?;
match formats[0] { match formats[0] {
Gzip | Bzip | Lzma | Zstd => { Gzip | Bzip | Lzma | Zstd => {
reader = chain_reader_decoder(&formats[0], reader)?; reader = chain_reader_decoder(&formats[0], reader)?;
@ -298,16 +346,33 @@ fn decompress_file(
let mut writer = fs::File::create(&output_path)?; let mut writer = fs::File::create(&output_path)?;
io::copy(&mut reader, &mut writer)?; 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 => { Tar => {
utils::create_dir_if_non_existent(output_folder)?; let _ = crate::archive::tar::unpack_archive(reader, output_folder, question_policy)?;
let _ = crate::archive::tar::unpack_archive(reader, output_folder, flags)?; info!("Successfully decompressed archive in {}.", nice_directory_display(output_folder));
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, 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 => { Zip => {
utils::create_dir_if_non_existent(output_folder)?;
eprintln!("Compressing first into .zip."); eprintln!("Compressing first into .zip.");
eprintln!("Warning: .zip archives with extra extensions have a downside."); eprintln!("Warning: .zip archives with extra extensions have a downside.");
eprintln!( eprintln!(
@ -319,9 +384,9 @@ fn decompress_file(
io::copy(&mut reader, &mut vec)?; io::copy(&mut reader, &mut vec)?;
let zip_archive = zip::ZipArchive::new(io::Cursor::new(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));
} }
} }

View File

@ -29,7 +29,7 @@ impl<'a> Confirmation<'a> {
}; };
loop { 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()?; io::stdout().flush()?;
let mut answer = String::new(); let mut answer = String::new();

View File

@ -11,7 +11,7 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use crate::{oof, utils::colors::*}; use crate::utils::colors::*;
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub enum Error { pub enum Error {
@ -24,7 +24,6 @@ pub enum Error {
PermissionDenied { error_title: String }, PermissionDenied { error_title: String },
UnsupportedZipArchive(&'static str), UnsupportedZipArchive(&'static str),
InternalError, InternalError,
OofError(oof::OofError),
CompressingRootFolder, CompressingRootFolder,
MissingArgumentsForCompression, MissingArgumentsForCompression,
MissingArgumentsForDecompression, MissingArgumentsForDecompression,
@ -45,11 +44,11 @@ pub struct FinalError {
impl Display for FinalError { impl Display for FinalError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Title // Title
writeln!(f, "{}[ERROR]{} {}", red(), reset(), self.title)?; writeln!(f, "{}[ERROR]{} {}", *RED, *RESET, self.title)?;
// Details // Details
for detail in &self.details { for detail in &self.details {
writeln!(f, " {}-{} {}", white(), yellow(), detail)?; writeln!(f, " {}-{} {}", *WHITE, *YELLOW, detail)?;
} }
// Hints // Hints
@ -57,11 +56,11 @@ impl Display for FinalError {
// Separate by one blank line. // Separate by one blank line.
writeln!(f)?; writeln!(f)?;
for hint in &self.hints { 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![] } 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.details.push(detail.to_string());
self 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.hints.push(hint.to_string());
self self
} }
@ -89,70 +88,53 @@ impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let err = match self { let err = match self {
Error::MissingExtensionError(filename) => { 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") .detail("Ouch could not detect the compression format")
.hint("Use a supported format extension, like '.zip' or '.tar.gz'") .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") .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::WalkdirError { reason } => FinalError::with_title(reason),
Error::FileNotFound(file) => { Error::FileNotFound(file) => {
let error = if file == Path::new("") { if file == Path::new("") {
FinalError::with_title("file not found!") FinalError::with_title("file not found!")
} else { } else {
FinalError::with_title(format!("file {:?} not found!", file)) FinalError::with_title(format!("file {:?} not found!", file))
}; }
error
} }
Error::CompressingRootFolder => { 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.") .detail("This is unadvisable since ouch does compressions in-memory.")
.hint("Use a more appropriate tool for this, such as rsync.") .hint("Use a more appropriate tool for this, such as rsync.")
.into_owned();
error
} }
Error::MissingArgumentsForCompression => { 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") .detail("The compress command requires at least 2 arguments")
.hint("You must provide:") .hint("You must provide:")
.hint(" - At least one input argument.") .hint(" - At least one input argument.")
.hint(" - The output argument.") .hint(" - The output argument.")
.hint("") .hint("")
.hint("Example: `ouch compress image.png img.zip`") .hint("Example: `ouch compress image.png img.zip`")
.into_owned();
error
} }
Error::MissingArgumentsForDecompression => { 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") .detail("The compress command requires at least one argument")
.hint("You must provide:") .hint("You must provide:")
.hint(" - At least one input argument.") .hint(" - At least one input argument.")
.hint("") .hint("")
.hint("Example: `ouch decompress imgs.tar.gz`") .hint("Example: `ouch decompress imgs.tar.gz`")
.into_owned();
error
} }
Error::InternalError => { Error::InternalError => {
let error = FinalError::with_title("InternalError :(") FinalError::with_title("InternalError :(")
.detail("This should not have happened") .detail("This should not have happened")
.detail("It's probably our fault") .detail("It's probably our fault")
.detail("Please help us improve by reporting the issue at:") .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))
.into_owned();
error
} }
Error::OofError(err) => FinalError::with_title(err),
Error::IoError { reason } => FinalError::with_title(reason), Error::IoError { reason } => FinalError::with_title(reason),
Error::CompressionTypo => FinalError::with_title("Possible typo detected") Error::CompressionTypo => {
.hint(format!("Did you mean '{}ouch compress{}'?", magenta(), reset())) FinalError::with_title("Possible typo detected")
.into_owned(), .hint(format!("Did you mean '{}ouch compress{}'?", *MAGENTA, *RESET))
}
Error::UnknownExtensionError(_) => todo!(), Error::UnknownExtensionError(_) => todo!(),
Error::AlreadyExists => todo!(), Error::AlreadyExists => todo!(),
Error::InvalidZipArchive(_) => todo!(), Error::InvalidZipArchive(_) => todo!(),
@ -202,12 +184,6 @@ impl From<walkdir::Error> for Error {
} }
} }
impl From<oof::OofError> for Error {
fn from(err: oof::OofError) -> Self {
Self::OofError(err)
}
}
impl From<FinalError> for Error { impl From<FinalError> for Error {
fn from(err: FinalError) -> Self { fn from(err: FinalError) -> Self {
Self::Custom { reason: err } Self::Custom { reason: err }

View File

@ -1,6 +1,6 @@
//! Our representation of all the supported compression formats. //! Our representation of all the supported compression formats.
use std::{fmt, path::Path}; use std::{ffi::OsStr, fmt, path::Path};
use self::CompressionFormat::*; use self::CompressionFormat::*;
@ -11,6 +11,10 @@ pub enum CompressionFormat {
Bzip, // .bz Bzip, // .bz
Lzma, // .lzma Lzma, // .lzma
Tar, // .tar (technically not a compression extension, but will do for now) Tar, // .tar (technically not a compression extension, but will do for now)
Tgz, // .tgz
Tbz, // .tbz
Tlzma, // .tlzma
Tzst, // .tzst
Zstd, // .zst Zstd, // .zst
Zip, // .zip Zip, // .zip
} }
@ -26,6 +30,10 @@ impl fmt::Display for CompressionFormat {
Zstd => ".zst", Zstd => ".zst",
Lzma => ".lz", Lzma => ".lz",
Tar => ".tar", Tar => ".tar",
Tgz => ".tgz",
Tbz => ".tbz",
Tlzma => ".tlz",
Tzst => ".tzst",
Zip => ".zip", Zip => ".zip",
} }
) )
@ -44,18 +52,20 @@ pub fn separate_known_extensions_from_name(mut path: &Path) -> (&Path, Vec<Compr
let mut extensions = vec![]; let mut extensions = vec![];
// While there is known extensions at the tail, grab them // While there is known extensions at the tail, grab them
while let Some(extension) = path.extension() { while let Some(extension) = path.extension().and_then(OsStr::to_str) {
let extension = match () { extensions.push(match extension {
_ if extension == "tar" => Tar, "tar" => Tar,
_ if extension == "zip" => Zip, "tgz" => Tgz,
_ if extension == "bz" || extension == "bz2" => Bzip, "tbz" | "tbz2" => Tbz,
_ if extension == "gz" => Gzip, "txz" | "tlz" | "tlzma" => Tlzma,
_ if extension == "xz" || extension == "lzma" || extension == "lz" => Lzma, "tzst" => Tzst,
_ if extension == "zst" => Zstd, "zip" => Zip,
"bz" | "bz2" => Bzip,
"gz" => Gzip,
"xz" | "lzma" | "lz" => Lzma,
"zst" => Zstd,
_ => break, _ => break,
}; });
extensions.push(extension);
// Update for the next iteration // Update for the next iteration
path = if let Some(stem) = path.file_stem() { Path::new(stem) } else { Path::new("") }; path = if let Some(stem) = path.file_stem() { Path::new(stem) } else { Path::new("") };

View File

@ -7,7 +7,6 @@
// Public modules // Public modules
pub mod cli; pub mod cli;
pub mod commands; pub mod commands;
pub mod oof;
// Private modules // Private modules
pub mod archive; pub mod archive;
@ -19,60 +18,5 @@ mod utils;
pub use error::{Error, Result}; pub use error::{Error, Result};
use lazy_static::lazy_static;
/// The status code ouch has when an error is encountered /// The status code ouch has when an error is encountered
pub const EXIT_FAILURE: i32 = libc::EXIT_FAILURE; 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}<files...>{reset} Decompresses files.
{green}ouch compress {magenta}<files...> 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());
}

View File

@ -1,24 +1,13 @@
use crate::NO_COLOR_IS_SET;
#[macro_export] #[macro_export]
macro_rules! info { macro_rules! info {
($writer:expr, $($arg:tt)*) => { ($($arg:tt)*) => {
use crate::macros::_info_helper; $crate::macros::_info_helper();
_info_helper(); println!($($arg)*);
println!($writer, $($arg)*);
};
($writer:expr) => {
_info_helper();
println!($writer);
}; };
} }
pub fn _info_helper() { pub fn _info_helper() {
use crate::utils::colors::{reset, yellow}; use crate::utils::colors::{RESET, YELLOW};
if *NO_COLOR_IS_SET { print!("{}[INFO]{} ", *YELLOW, *RESET);
print!("[INFO] ");
} else {
print!("{}[INFO]{} ", yellow(), reset());
}
} }

View File

@ -1,7 +1,4 @@
use ouch::{ use ouch::{cli::Opts, commands, Result};
cli::{parse_args, ParsedArgs},
commands, Result,
};
fn main() { fn main() {
if let Err(err) = run() { if let Err(err) = run() {
@ -10,7 +7,7 @@ fn main() {
} }
} }
fn run() -> crate::Result<()> { fn run() -> Result<()> {
let ParsedArgs { command, flags } = parse_args()?; let (args, skip_questions_positively) = Opts::parse_args()?;
commands::run(command, &flags) commands::run(args, skip_questions_positively)
} }

View File

@ -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),
}
}
}

View File

@ -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<char>,
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<OsString> {
self.argument_flags.remove(flag_name)
}
}
#[derive(Debug)]
pub enum FlagType {
None,
Short,
Long,
}
impl FlagType {
pub fn from(text: impl AsRef<OsStr>) -> 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
}
}
}

View File

@ -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<String>` (or `BTreeSet<&str>`).
pub fn pop_subcommand<'a, T, I, II>(args: &mut Vec<T>, subcommands: I) -> Option<&'a II>
where
I: IntoIterator<Item = &'a II>,
II: AsRef<str>,
T: AsRef<OsStr>,
{
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<OsString>, flags_info: &[Flag]) -> Result<(Vec<OsString>, Flags), OofError> {
let mut short_flags_info = BTreeMap::<char, &Flag>::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::<Vec<char>>();
// 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<T, U>(args: &[T], texts: &[U]) -> bool
where
T: AsRef<OsStr>,
U: AsRef<str>,
{
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<OsString> {
let args = text.split_whitespace();
args.map(OsString::from).collect()
}
fn setup_args_scenario(arg_str: &str) -> Result<(Vec<OsString>, 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)
}};
}

View File

@ -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");
}
}

View File

@ -1,18 +1,19 @@
use std::{ use std::{
cmp, env, cmp, env,
ffi::OsStr, ffi::OsStr,
path::Component,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use fs_err as fs; 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. /// Checks if the given path represents an empty directory.
pub fn dir_is_empty(dir_path: &Path) -> bool { pub fn dir_is_empty(dir_path: &Path) -> bool {
let is_empty = |mut rd: std::fs::ReadDir| rd.next().is_none(); 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<()> { 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(()) 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 /// Changes the process' current directory to the directory that contains the
/// file pointed to by `filename` and returns the directory that the process /// file pointed to by `filename` and returns the directory that the process
/// was in before this function was called. /// was in before this function was called.
@ -36,22 +44,17 @@ pub fn cd_into_same_dir_as(filename: &Path) -> crate::Result<PathBuf> {
Ok(previous_location) Ok(previous_location)
} }
pub fn user_wants_to_overwrite(path: &Path, flags: &oof::Flags) -> crate::Result<bool> { pub fn user_wants_to_overwrite(path: &Path, question_policy: QuestionPolicy) -> crate::Result<bool> {
match (flags.is_present("yes"), flags.is_present("no")) { match question_policy {
(true, true) => { QuestionPolicy::AlwaysYes => Ok(true),
unreachable!("This should've been cutted out in the ~/src/cli.rs filter flags function.") 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<OsStr>) -> String { pub fn to_utf(os_str: impl AsRef<OsStr>) -> String {
@ -59,58 +62,46 @@ pub fn to_utf(os_str: impl AsRef<OsStr>) -> String {
text.trim_matches('"').to_string() text.trim_matches('"').to_string()
} }
pub fn nice_directory_display(os_str: impl AsRef<OsStr>) -> String {
let text = to_utf(os_str);
if text == "." {
"current directory".to_string()
} else {
format!("'{}'", text)
}
}
pub struct Bytes { pub struct Bytes {
bytes: f64, bytes: f64,
} }
/// Module with a list of bright colors. /// Module with a list of bright colors.
#[allow(dead_code)] #[allow(dead_code)]
pub mod colors {
use once_cell::sync::Lazy;
static DISABLE_COLORED_TEXT: Lazy<bool> = 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")] #[cfg(target_family = "unix")]
pub mod colors { pub static $name: Lazy<&str> = Lazy::new(|| if *DISABLE_COLORED_TEXT { "" } else { $value });
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"))] #[cfg(not(target_family = "unix"))]
pub mod colors { pub static $name: &&str = &"";
pub const fn empty() -> &'static str { };
""
} }
pub const reset: fn() -> &'static str = empty;
pub const black: fn() -> &'static str = empty; color!(RESET = "\u{1b}[39m");
pub const blue: fn() -> &'static str = empty; color!(BLACK = "\u{1b}[38;5;8m");
pub const cyan: fn() -> &'static str = empty; color!(BLUE = "\u{1b}[38;5;12m");
pub const green: fn() -> &'static str = empty; color!(CYAN = "\u{1b}[38;5;14m");
pub const magenta: fn() -> &'static str = empty; color!(GREEN = "\u{1b}[38;5;10m");
pub const red: fn() -> &'static str = empty; color!(MAGENTA = "\u{1b}[38;5;13m");
pub const white: fn() -> &'static str = empty; color!(RED = "\u{1b}[38;5;9m");
pub const yellow: fn() -> &'static str = empty; color!(WHITE = "\u{1b}[38;5;15m");
color!(YELLOW = "\u{1b}[38;5;11m");
} }
impl Bytes { 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -9,7 +9,10 @@ use std::{
use fs_err as fs; 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 rand::{rngs::SmallRng, RngCore, SeedableRng};
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
use utils::*; use utils::*;
@ -29,12 +32,22 @@ fn sanity_check_through_mime() {
let bytes = generate_random_file_content(&mut SmallRng::from_entropy()); let bytes = generate_random_file_content(&mut SmallRng::from_entropy());
test_file.write_all(&bytes).expect("to successfully write bytes to the file"); 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 = [ let expected_mimes = [
"application/x-tar", "application/x-tar",
"application/zip", "application/zip",
"application/gzip", "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-bzip2", "application/x-bzip2",
"application/x-xz", "application/x-xz",
@ -69,6 +82,13 @@ fn test_each_format() {
test_compressing_and_decompressing_archive("tar.lz"); test_compressing_and_decompressing_archive("tar.lz");
test_compressing_and_decompressing_archive("tar.lzma"); test_compressing_and_decompressing_archive("tar.lzma");
test_compressing_and_decompressing_archive("tar.zst"); 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");
test_compressing_and_decompressing_archive("zip.gz"); test_compressing_and_decompressing_archive("zip.gz");
test_compressing_and_decompressing_archive("zip.bz"); 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(); (0..quantity_of_files).map(|_| generate_random_file_content(&mut rng)).collect();
// Create them // 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 // 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 // Decompress them
let mut extracted_paths = extract_files(&compressed_archive_path); let mut extracted_paths = extract_files(&compressed_archive_path);
@ -157,11 +177,15 @@ fn extract_files(archive_path: &Path) -> Vec<PathBuf> {
// Add the suffix "results" // Add the suffix "results"
extraction_output_folder.push("extraction_results"); extraction_output_folder.push("extraction_results");
let command = Command::Decompress { let command = Opts {
yes: false,
no: false,
cmd: Subcommand::Decompress {
files: vec![archive_path.to_owned()], files: vec![archive_path.to_owned()],
output_folder: Some(extraction_output_folder.clone()), 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() fs::read_dir(extraction_output_folder).unwrap().map(Result::unwrap).map(|entry| entry.path()).collect()
} }

View File

@ -20,11 +20,11 @@ fn test_compress_decompress_with_empty_dir(format: &str) {
let testing_dir_path = testing_dir.path(); 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<PathBuf> = vec![empty_dir_path]; let mut file_paths: Vec<PathBuf> = 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); let mut extracted_paths = extract_files(&compressed_archive_path);

View File

@ -6,7 +6,10 @@ use std::path::{Path, PathBuf};
use fs_err as fs; 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 { pub fn create_empty_dir(at: &Path, filename: &str) -> PathBuf {
let dirname = Path::new(filename); 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 = String::from("archive.") + format;
let archive_path = at.join(archive_path); 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 = Opts {
run(command, &oof::Flags::default()).expect("Failed to compress test dummy files"); 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 archive_path
} }
@ -39,11 +46,15 @@ pub fn extract_files(archive_path: &Path) -> Vec<PathBuf> {
// Add the suffix "results" // Add the suffix "results"
extraction_output_folder.push("extraction_results"); extraction_output_folder.push("extraction_results");
let command = Command::Decompress { let command = Opts {
yes: false,
no: false,
cmd: Subcommand::Decompress {
files: vec![archive_path.to_owned()], files: vec![archive_path.to_owned()],
output_folder: Some(extraction_output_folder.clone()), 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() fs::read_dir(extraction_output_folder).unwrap().map(Result::unwrap).map(|entry| entry.path()).collect()
} }