mirror of
https://github.com/ouch-org/ouch.git
synced 2025-06-06 11:35:45 +00:00
Merge branch 'master' into issue-56
This commit is contained in:
commit
704a4efdd7
23
.github/workflows/build.yml
vendored
23
.github/workflows/build.yml
vendored
@ -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
|
||||
|
15
CONTRIBUTING.md
Normal file
15
CONTRIBUTING.md
Normal 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
196
Cargo.lock
generated
@ -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 = "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]]
|
||||
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",
|
||||
@ -366,25 +467,64 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.26"
|
||||
name = "termcolor"
|
||||
version = "1.1.2"
|
||||
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 = [
|
||||
"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"
|
||||
|
@ -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"
|
||||
|
113
README.md
113
README.md
@ -2,124 +2,101 @@
|
||||
|
||||
[](https://crates.io/crates/ouch) [](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.
|
||||
|
||||
<!-- TODO -->
|
||||
<!-- - [Listing files](#Listing-the-elements-of-an-archive) -->
|
||||
`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. <!-- We should post benchmarks in our wiki and link them here -->
|
||||
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
|
||||
# Short alternative
|
||||
ouch c file.txt file.zip
|
||||
|
||||
# Compress folder and video into 'videos.tar.gz'
|
||||
ouch compress videos/ meme.mp4 videos.tar.gz
|
||||
|
||||
# Compress one file using 4 compression formats
|
||||
ouch compress file.txt compressed.gz.xz.bz.zst
|
||||
|
||||
# Compress all the files in current folder
|
||||
ouch compress * files.zip
|
||||
# Compress everything in the current folder again and again
|
||||
ouch compress * everything.tar.gz.xz.bz.zst.gz.gz.gz.gz.gz
|
||||
```
|
||||
|
||||
`ouch` checks for the extensions of the **output file** to decide which formats should be used.
|
||||
|
||||
<!-- ### Listing the elements of an archive
|
||||
|
||||
* **Upcoming feature**
|
||||
|
||||
```
|
||||
# Shows the files and folders contained in videos.tar.xz
|
||||
ouch list videos.tar.xz
|
||||
``` -->
|
||||
|
||||
## Installation
|
||||
|
||||
[](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 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
|
||||
Note that formats can be chained:
|
||||
- `.tar.gz`
|
||||
- `.tar.xz`
|
||||
- `.tar.gz.xz`
|
||||
- `.tar.gz.gz.gz.gz.gz.gz.gz.gz.gz.gz.gz.gz.gz.gz.gz.gz.gz.gz.lz.lz.lz.lz.lz.lz.lz.lz.lz.lz.bz.bz.bz.bz.bz.bz.bz`
|
||||
- `.gz.xz`
|
||||
- etc...
|
||||
And the aliases: `tgz`, `tbz`, `tbz2`, `txz`, `tlz`, `tlzma`, `tzst`.
|
||||
|
||||
Formats can be chained (`ouch` keeps it _fast_):
|
||||
|
||||
- `.gz.xz.bz.zst`
|
||||
- `.tar.gz.xz.bz.zst`
|
||||
- `.tar.gz.gz.gz.gz.xz.xz.xz.xz.bz.bz.bz.bz.zst.zst.zst.zst`
|
||||
|
||||
## Contributing
|
||||
|
||||
`ouch` is 100% made out of voluntary work, any small contribution is welcome!
|
||||
|
||||
- Open an issue.
|
||||
- Open a pr.
|
||||
- Share it to a friend.
|
||||
- Open a pull request.
|
||||
- Share it to a friend!
|
||||
|
1
rust-toolchain
Normal file
1
rust-toolchain
Normal file
@ -0,0 +1 @@
|
||||
nightly
|
@ -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
|
||||
|
@ -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<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 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 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;
|
||||
}
|
||||
|
||||
|
@ -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<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
|
||||
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()));
|
||||
|
||||
|
224
src/cli.rs
224
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<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 {
|
||||
/// Files to be decompressed
|
||||
#[clap(required = true, min_values = 1)]
|
||||
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)
|
||||
///
|
||||
/// 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<ParsedArgs> {
|
||||
// From argv, but ignoring empty arguments
|
||||
let args = env::args_os().skip(1).filter(|arg| !arg.is_empty()).collect();
|
||||
let mut parsed_args = parse_args_from(args)?;
|
||||
impl Opts {
|
||||
/// A helper method that calls `clap::Parser::parse` and then translates relative paths to absolute.
|
||||
/// Also determines if the user wants to skip questions or not
|
||||
pub fn parse_args() -> crate::Result<(Self, QuestionPolicy)> {
|
||||
let mut opts: Self = Self::parse();
|
||||
|
||||
// If has a list of files, canonicalize them, reporting error if they do not exist
|
||||
match &mut parsed_args.command {
|
||||
Command::Compress { files, .. } | Command::Decompress { files, .. } => {
|
||||
*files = canonicalize_files(files)?;
|
||||
}
|
||||
_ => {}
|
||||
let (Subcommand::Compress { files, .. } | Subcommand::Decompress { files, .. }) = &mut opts.cmd;
|
||||
*files = canonicalize_files(files)?;
|
||||
|
||||
let skip_questions_positively = if opts.yes {
|
||||
QuestionPolicy::AlwaysYes
|
||||
} else if opts.no {
|
||||
QuestionPolicy::AlwaysNo
|
||||
} else {
|
||||
QuestionPolicy::Ask
|
||||
};
|
||||
|
||||
Ok((opts, skip_questions_positively))
|
||||
}
|
||||
|
||||
if parsed_args.flags.is_present("yes") && parsed_args.flags.is_present("no") {
|
||||
todo!("conflicting flags, better error message.");
|
||||
}
|
||||
|
||||
Ok(parsed_args)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ParsedArgs {
|
||||
pub command: Command,
|
||||
pub flags: oof::Flags,
|
||||
}
|
||||
|
||||
/// Checks if the first argument is a typo for the `compress` subcommand.
|
||||
/// Returns true if the arg is probably a typo or false otherwise.
|
||||
fn is_typo(path: impl AsRef<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> {
|
||||
@ -90,120 +89,3 @@ fn canonicalize(path: impl AsRef<Path>) -> crate::Result<PathBuf> {
|
||||
fn canonicalize_files(files: &[impl AsRef<Path>]) -> crate::Result<Vec<PathBuf>> {
|
||||
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(),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
167
src/commands.rs
167
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<PathBuf>,
|
||||
formats: Vec<CompressionFormat>,
|
||||
output_file: fs::File,
|
||||
_flags: &oof::Flags,
|
||||
) -> crate::Result<()> {
|
||||
fn compress_files(files: Vec<PathBuf>, formats: Vec<CompressionFormat>, 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<dyn Write> = 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<extension::CompressionFormat>,
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
62
src/error.rs
62
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<walkdir::Error> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<oof::OofError> for Error {
|
||||
fn from(err: oof::OofError) -> Self {
|
||||
Self::OofError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FinalError> for Error {
|
||||
fn from(err: FinalError) -> Self {
|
||||
Self::Custom { reason: err }
|
||||
|
@ -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<Compr
|
||||
let mut extensions = vec![];
|
||||
|
||||
// While there is known extensions at the tail, grab them
|
||||
while let Some(extension) = path.extension() {
|
||||
let extension = match () {
|
||||
_ if extension == "tar" => 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("") };
|
||||
|
56
src/lib.rs
56
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}<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());
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
11
src/main.rs
11
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)
|
||||
}
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
109
src/oof/flags.rs
109
src/oof/flags.rs
@ -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
|
||||
}
|
||||
}
|
||||
}
|
383
src/oof/mod.rs
383
src/oof/mod.rs
@ -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)
|
||||
}};
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
122
src/utils.rs
122
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<PathBuf> {
|
||||
Ok(previous_location)
|
||||
}
|
||||
|
||||
pub fn user_wants_to_overwrite(path: &Path, flags: &oof::Flags) -> crate::Result<bool> {
|
||||
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<bool> {
|
||||
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<OsStr>) -> String {
|
||||
@ -59,58 +62,46 @@ pub fn to_utf(os_str: impl AsRef<OsStr>) -> 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 {
|
||||
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"
|
||||
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")]
|
||||
pub static $name: Lazy<&str> = Lazy::new(|| if *DISABLE_COLORED_TEXT { "" } else { $value });
|
||||
#[cfg(not(target_family = "unix"))]
|
||||
pub static $name: &&str = &"";
|
||||
};
|
||||
}
|
||||
pub const fn black() -> &'static str {
|
||||
"\u{1b}[38;5;8m"
|
||||
}
|
||||
pub const fn blue() -> &'static str {
|
||||
"\u{1b}[38;5;12m"
|
||||
}
|
||||
pub const fn cyan() -> &'static str {
|
||||
"\u{1b}[38;5;14m"
|
||||
}
|
||||
pub const fn green() -> &'static str {
|
||||
"\u{1b}[38;5;10m"
|
||||
}
|
||||
pub const fn magenta() -> &'static str {
|
||||
"\u{1b}[38;5;13m"
|
||||
}
|
||||
pub const fn red() -> &'static str {
|
||||
"\u{1b}[38;5;9m"
|
||||
}
|
||||
pub const fn white() -> &'static str {
|
||||
"\u{1b}[38;5;15m"
|
||||
}
|
||||
pub const fn yellow() -> &'static str {
|
||||
"\u{1b}[38;5;11m"
|
||||
}
|
||||
}
|
||||
// Windows does not support ANSI escape codes
|
||||
#[allow(dead_code, non_upper_case_globals)]
|
||||
#[cfg(not(target_family = "unix"))]
|
||||
pub mod colors {
|
||||
pub const fn empty() -> &'static str {
|
||||
""
|
||||
}
|
||||
pub const reset: fn() -> &'static str = empty;
|
||||
pub const black: fn() -> &'static str = empty;
|
||||
pub const blue: fn() -> &'static str = empty;
|
||||
pub const cyan: fn() -> &'static str = empty;
|
||||
pub const green: fn() -> &'static str = empty;
|
||||
pub const magenta: fn() -> &'static str = empty;
|
||||
pub const red: fn() -> &'static str = empty;
|
||||
pub const white: fn() -> &'static str = empty;
|
||||
pub const yellow: fn() -> &'static str = empty;
|
||||
|
||||
color!(RESET = "\u{1b}[39m");
|
||||
color!(BLACK = "\u{1b}[38;5;8m");
|
||||
color!(BLUE = "\u{1b}[38;5;12m");
|
||||
color!(CYAN = "\u{1b}[38;5;14m");
|
||||
color!(GREEN = "\u{1b}[38;5;10m");
|
||||
color!(MAGENTA = "\u{1b}[38;5;13m");
|
||||
color!(RED = "\u{1b}[38;5;9m");
|
||||
color!(WHITE = "\u{1b}[38;5;15m");
|
||||
color!(YELLOW = "\u{1b}[38;5;11m");
|
||||
}
|
||||
|
||||
impl Bytes {
|
||||
@ -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::*;
|
||||
|
@ -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<PathBuf> {
|
||||
// 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()
|
||||
}
|
||||
|
@ -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<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);
|
||||
|
||||
|
@ -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<PathBuf> {
|
||||
// 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()
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user