Compare commits

...

15 Commits

Author SHA1 Message Date
andy.boot bc0e376c88 New Version 2020-02-19 20:22:54 +00:00
andy.boot d7c602a2d2 Merge pull request #72 from bootandy/ab-redesign
Large redesign
2020-02-19 20:19:50 +00:00
andy.boot 603e6be7eb Large redesign
Use the whole width of the terminal assume width of 80 if none found

Show percentages in the final column. Show ASCII bars indicating usage
of the underlying directories in the space inbetween.

Display (height of terminal - 10) entries by default.

Reverse the output order so largest is at the bottom.

Break up tests. Change older tests to check real output of program.
2020-02-18 20:58:53 +00:00
andy.boot efa469e12f Merge pull request #67 from bootandy/ab-simplify-inodes2
Simplify inodes & devices by removing Option
2020-02-10 22:11:35 +00:00
andy.boot 657858df16 Simplify inodes & devices by removing Option
This code now won't compile on none-(windows/unix family)
systems. [unix family includes mac]

Removing this method allows us to remove an Option and simplify the code
slightly
2020-02-09 14:33:46 +00:00
andy.boot a8d700d530 Merge pull request #66 from bootandy/ab-refactor
Refactor code
2020-02-09 14:27:05 +00:00
andy.boot c408d8887d Update docs for -X flag.
-X flag changed subtly with previous pull request. It now requires a
directory name and doesn't work on substrings.
2020-02-09 14:10:53 +00:00
andy.boot be2250d241 Refactor: use if let instead of is_some 2020-02-09 14:10:53 +00:00
andy.boot b6aa1378de Fix bug for devices and apparent size
If apparent_size was set and ignore files on other devices was set then
the latter flat would not work.

Fix this bug
2020-02-09 13:57:08 +00:00
andy.boot 2082141dfc Add tests for should_ignore_file function
function currently has a bug that is highlighted by second test
2020-02-09 13:57:08 +00:00
andy.boot 8a9b5e889d Refactor path depth calculation
Factor out duplicate code,
Add comment explaining why filter is necessary (thanks to rivy)
2020-02-09 13:57:08 +00:00
andy.boot 871b7e90d8 Merge pull request #64 from rivy/rf.path
Refactor ~ use Path/PathBuf instead of &str/String
2020-02-09 13:52:11 +00:00
andy.boot edf300893c Increment version 2020-02-09 10:08:41 +00:00
Roy Ivy III a3d8fc00e1 Refactor ~ use AsRef<Path> where possible 2020-02-03 16:56:50 -06:00
Roy Ivy III 9d4531d48b Refactor ~ use PathBuf instead of String 2020-02-03 16:56:47 -06:00
14 changed files with 820 additions and 741 deletions
-83
View File
@@ -1,83 +0,0 @@
# Based on the "trust" template v0.1.2
# https://github.com/japaric/trust/tree/v0.1.2
# ----------- To do a release ---------
# tag a commit and push:
# git tag v0.4.0.1
# git push origin v0.4.0.1
# Remember to do a cargo publish to put it in crates.io
dist: trusty
language: rust
services: docker
sudo: required
# TODO Rust builds on stable by default, this can be
# overridden on a case by case basis down below.
env:
global:
# TODO Update this to match the name of your project.
- CRATE_NAME=dust
matrix:
# TODO These are all the build jobs. Adjust as necessary. Comment out what you
# don't need
include:
# Linux
- env: TARGET=x86_64-unknown-linux-gnu
# OSX
- env: TARGET=x86_64-apple-darwin
os: osx
before_install:
- set -e
- rustup self update
install:
- sh ci/install.sh
- source ~/.cargo/env || true
script:
- bash ci/script.sh
after_script: set +e
before_deploy:
- sh ci/before_deploy.sh
deploy:
# TODO update `api_key.secure`
# - Create a `public_repo` GitHub token. Go to: https://github.com/settings/tokens/new
# - Encrypt it: `travis encrypt 0123456789012345678901234567890123456789
# - Paste the output down here
# api_key:
# secure: UlU73Td7Bkb2N88ws4YGLWR+4U0IMgiou9QQtMnmpouJFjeUNxtLSPMPODVXP7zq4sKt5HR5B3fX9MW4mKm351fvnQEoihETn06pKiXGnY//SlTPTt67MX9ZOYmd9ohJReMDOZDgqhnGLxfymycGtsLAmdjDZnAl+IMqgg0FMyVFj9Cl9aKxnn12lxQyX4zabHKk8TUKD3By8ZoEUnJMHt3gEtOmbDgS4brcTPeHCzqnYFw73LEnkqvz+JP0XwauJY7Cf8lminKm/klmjCkQji8T9SHI52v1g0Fxpx0ucp2o3vulQrLHXaHvZ6Fr7J0cSXXzaFF3rrGLt4t4jU/+9TZm1+n5k5XuPW4x4NTCC9NmIj/z0/z41t82E9qZhzhtm2Jdsg6H2tNk+C774TYqcmR6GCvfRadfjRp3cA5dh0UwDVjH2MJFxlHDVkl6la0mVVRsCGF3oBKZVk0BDl1womfnmI46o/uU+gLknHN6Ed6PHHPPYDViWd3VKdmHKT7XrkMMUF6HjZUtla689DWIOWZSiV++1dVPcl/1TV+6tTmN4bBtPcLuX7SHRuLp2PI2kATvRMECsa7gZRypW4jKpVn7b2yetX9TVI3i1zR5zkQJ3dPg8sATvYPL53aKH/WsqUg4rzoAlbk9so+++R4bQY69LhV3B511B7EAynoZFdM
api_key: $API_KEY
file_glob: true
file: $CRATE_NAME-$TRAVIS_TAG-$TARGET.*
on:
# TODO Here you can pick which targets will generate binary releases
# In this example, there are some targets that are tested using the stable
# and nightly channels. This condition makes sure there is only one release
# for such targets and that's generated using the stable channel
condition: $TRAVIS_RUST_VERSION = stable
tags: true
provider: releases
skip_cleanup: true
cache: cargo
before_cache:
# Travis can't cache files that are not readable by "others"
- chmod -R a+r $HOME/.cargo
branches:
only:
# release tags
- /^v\d+\.\d+\.\d+.*$/
- master
notifications:
email:
on_success: never
Generated
+26 -14
View File
@@ -26,7 +26,7 @@ dependencies = [
"environment 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"failure_derive 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.44 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.47 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -46,7 +46,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "backtrace"
version = "0.3.42"
version = "0.3.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"backtrace-sys 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -180,7 +180,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "du-dust"
version = "0.4.4"
version = "0.5.0"
dependencies = [
"ansi_term 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)",
"assert_cli 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -188,6 +188,8 @@ dependencies = [
"jwalk 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"num_cpus 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"terminal_size 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -206,7 +208,7 @@ name = "failure"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"backtrace 0.3.42 (registry+https://github.com/rust-lang/crates.io-index)",
"backtrace 0.3.44 (registry+https://github.com/rust-lang/crates.io-index)",
"failure_derive 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -217,7 +219,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)",
"synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -241,7 +243,7 @@ dependencies = [
[[package]]
name = "itoa"
version = "0.4.4"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
@@ -416,10 +418,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "serde_json"
version = "1.0.44"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
"itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
"ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -431,7 +433,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "syn"
version = "1.0.13"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -446,7 +448,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -463,6 +465,15 @@ dependencies = [
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "terminal_size"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "textwrap"
version = "0.11.0"
@@ -524,7 +535,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum assert_cli 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a29ab7c0ed62970beb0534d637a8688842506d0ff9157de83286dacd065c8149"
"checksum atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
"checksum autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2"
"checksum backtrace 0.3.42 (registry+https://github.com/rust-lang/crates.io-index)" = "b4b1549d804b6c73f4817df2ba073709e96e426f12987127c48e6745568c350b"
"checksum backtrace 0.3.44 (registry+https://github.com/rust-lang/crates.io-index)" = "e4036b9bf40f3cf16aba72a3d65e8a520fc4bafcdc7079aea8f848c58c5b5536"
"checksum backtrace-sys 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)" = "5d6575f128516de27e3ce99689419835fce9643a9b215a14d2b5b685be018491"
"checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
"checksum c2-chacha 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "214238caa1bf3a496ec3392968969cab8549f96ff30652c9e56885329315f6bb"
@@ -545,7 +556,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum failure_derive 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0bc225b78e0391e4b8683440bf2e63c2deeeb2ce5189eab46e2b68c6d3725d08"
"checksum getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb"
"checksum hermit-abi 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "eff2656d88f158ce120947499e971d743c05dbcbed62e5bd2f38f1698bbc3772"
"checksum itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f"
"checksum itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e"
"checksum jwalk 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2b3dbf0a8f61baee43a2918ff50ac6a2d3b2c105bc08ed53bc298779f1263409"
"checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
"checksum libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)" = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558"
@@ -569,11 +580,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
"checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
"checksum serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)" = "414115f25f818d7dfccec8ee535d76949ae78584fc4f79a6f45a904bf8ab4449"
"checksum serde_json 1.0.44 (registry+https://github.com/rust-lang/crates.io-index)" = "48c575e0cc52bdd09b47f330f646cf59afc586e9c4e3ccd6fc1f625b8ea1dad7"
"checksum serde_json 1.0.47 (registry+https://github.com/rust-lang/crates.io-index)" = "15913895b61e0be854afd32fd4163fcd2a3df34142cf2cb961b310ce694cbf90"
"checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
"checksum syn 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)" = "1e4ff033220a41d1a57d8125eab57bf5263783dfdcc18688b1dacc6ce9651ef8"
"checksum syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)" = "af6f3550d8dff9ef7dc34d384ac6f107e5d31c8f57d9f28e0081503f547ac8f5"
"checksum synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545"
"checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9"
"checksum terminal_size 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "e25a60e3024df9029a414be05f46318a77c22538861a22170077d0388c0e926e"
"checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
"checksum unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479"
"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
+8 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "du-dust"
description = "A more intuitive version of du"
version = "0.4.4"
version = "0.5.0"
authors = ["bootandy <bootandy@gmail.com>", "nebkor <code@ardent.nebcorp.com>"]
edition = "2018"
@@ -25,6 +25,8 @@ ansi_term = "=0.12"
clap = "=2.33"
jwalk = "0.4.0"
num_cpus = "1.12"
terminal_size = "0.1.10"
unicode-width = "0.1.7"
[target.'cfg(windows)'.dependencies]
winapi-util = "0.1"
@@ -32,3 +34,8 @@ winapi-util = "0.1"
[dev-dependencies]
assert_cli = "=0.6"
tempfile = "=3"
[[test]]
name = "integration"
path = "tests/tests.rs"
+36 -19
View File
@@ -21,7 +21,7 @@ du + rust = dust. Like du but more intuitive
Dust is meant to give you an instant overview of which directories are using disk space without requiring sort or head. Dust will print a maximum of 1 'Did not have permissions message'.
Dust will list the 20 biggest sub directories or files and will smartly recurse down the tree to find the larger ones. There is no need for a '-d' flag or a '-h' flag. The largest sub directory will have its size shown in *red*
Dust will list the terminal height - 10 biggest sub directories or files and will smartly recurse down the tree to find the larger ones. There is no need for a '-d' flag or a '-h' flag. The largest sub directory will have its size shown in *red*
## Why?
@@ -41,27 +41,44 @@ Usage: dust -n 30 <dir> (Shows 30 directories not 20)
Usage: dust -d 3 <dir> (Shows 3 levels of subdirectories)
Usage: dust -r <dir> (Reverse order of output, with root at the lowest)
Usage: dust -x <dir> (Only show directories on same filesystem)
Usage: dust -X ignore <dir> (Ignore all files and directories containing the string 'ignore')
Usage: dust -X ignore <dir> (Ignore all files and directories with the name 'ignore')
Usage: dust -b <dir> (Do not show percentages or draw the ASCII bars)
```
```
djin:git/dust> dust
1.2G target
622M ├─ debug
445M │ ├── deps
70M │ ├── incremental
56M │ └── build
262M ├─┬ rls
262M │ └─┬ debug
203M │ ├── deps
56M │ └── build
165M ├─┬ package
165M │ └─┬ du-dust-0.2.4
165M └─┬ target
165M │ └─┬ debug
131M ── deps
165M └─┬ release
124M └── deps
$ dust target
15M ┌── build │ ░█ │ 2%
25M ├─ deps │ ░█ │ 4%
45M ┌─┴ release │ ██ │ 7%
84M │ ┌── build │ ▒▒▒▒▒████ │ 13%
7.6M │ │ ┌── libsynstructure-f7552412787ad339.rlib│ ▒▒▒▓▓▓▓▓█ │ 1%
16M │ │ ├── libfailure_derive-e18365d3e6be2e2c.so│ ▒▒▒▓▓▓▓▓█ │ 2%
18M │ │ ├── libsyn-9ad95b745845d5dd.rlib │ ▒▒▒▓▓▓▓▓█ │ 3%
19M ├── libsyn-d4a3458fcb1c592c.rlib │ ▒▒▒▓▓▓▓▓█ │ 3%
135M │ ├─┴ deps │ ▒▒▒██████ │ 20%
228M │ ┌─┴ debug │ █████████ │ 34%
228M ├─┴ rls │ █████████ │ 34%
18M │ ┌── dust │ ░░░░░░░░░░░░░░█ │ 3%
22M │ ├── dust-a0c31c4633c5fc8b │ ░░░░░░░░░░░░░░█ │ 3%
7.4M │ │ ── s-fkrj3vfncf-19aj951-1fv3o6tzvr348 │ ░░░░░░░░░░░░░▒█ │ 1%
7.4M │ │ ┌─┴ dust-1i3xquz5fns51 │ ░░░░░░░░░░░░░▒█ │ 1%
40M │ ├─┴ incremental │ ░░░░░░░░░░░░░██ │ 6%
41M │ ├── build │ ░░░░░░░░░░░░░██ │ 6%
7.6M │ │ ┌── libsynstructure-f7552412787ad339.rlib │ ░░░░▒▒▒▒▒▒▒▒▒▒█ │ 1%
8.2M │ │ ├── libserde-ab4b407a415bc8fc.rmeta │ ░░░░▒▒▒▒▒▒▒▒▒▒█ │ 1%
9.4M │ │ ├── libserde-ab4b407a415bc8fc.rlib │ ░░░░▒▒▒▒▒▒▒▒▒▒█ │ 1%
11M │ │ ├── tests_symlinks-bf063461b7be6a99 │ ░░░░▒▒▒▒▒▒▒▒▒▒█ │ 2%
11M │ │ ├── integration-08f999d253e3b70c │ ░░░░▒▒▒▒▒▒▒▒▒▒█ │ 2%
15M │ │ ├── dust-1c6e63725d641738 │ ░░░░▒▒▒▒▒▒▒▒▒▒█ │ 2%
16M │ │ ├── libfailure_derive-e18365d3e6be2e2c.so │ ░░░░▒▒▒▒▒▒▒▒▒▒█ │ 2%
18M │ │ ├── dust-3a419f62b84d73c1 │ ░░░░▒▒▒▒▒▒▒▒▒▒█ │ 3%
18M │ │ ├── dust-2bdf724d4a721d31 │ ░░░░▒▒▒▒▒▒▒▒▒▒█ │ 3%
18M │ │ ├── libsyn-9ad95b745845d5dd.rlib │ ░░░░▒▒▒▒▒▒▒▒▒▒█ │ 3%
23M │ │ ├── libclap-0dedc35af3ef0670.rlib │ ░░░░▒▒▒▒▒▒▒▒▒▒█ │ 3%
267M │ ├─┴ deps │ ░░░░███████████ │ 40%
392M ├─┴ debug │ ███████████████ │ 59%
667M ┌─┴ target │█████████████████████████ │ 100%
```
+6
View File
@@ -0,0 +1,6 @@
# ----------- To do a release ---------
# tag a commit and push:
# git tag v0.4.5
# git push origin v0.4.5
# cargo publish to put it in crates.io
+197 -77
View File
@@ -4,32 +4,32 @@ use self::ansi_term::Colour::Fixed;
use self::ansi_term::Style;
use crate::utils::Node;
use terminal_size::{terminal_size, Height, Width};
use unicode_width::UnicodeWidthStr;
use std::cmp::max;
use std::cmp::min;
use std::iter::repeat;
use std::path::Path;
static UNITS: [char; 4] = ['T', 'G', 'M', 'K'];
static BLOCKS: [char; 5] = ['█', '▓', '▒', '░', ' '];
static DEFAULT_TERMINAL_WIDTH: u16 = 80;
pub struct DisplayData {
pub short_paths: bool,
pub is_reversed: bool,
pub colors_on: bool,
pub base_size: u64,
pub longest_string_length: usize,
}
impl DisplayData {
fn get_first_chars(&self) -> &str {
if self.is_reversed {
"─┴"
} else {
"─┬"
}
}
#[allow(clippy::collapsible_if)]
fn get_tree_chars(
&self,
num_siblings: u64,
max_siblings: u64,
has_children: bool,
) -> &'static str {
fn get_tree_chars(&self, was_i_last: bool, has_children: bool) -> &'static str {
if self.is_reversed {
if num_siblings == max_siblings - 1 {
if was_i_last {
if has_children {
"┌─┴"
} else {
@@ -41,7 +41,7 @@ impl DisplayData {
"├──"
}
} else {
if num_siblings == 0 {
if was_i_last {
if has_children {
"└─┬"
} else {
@@ -55,22 +55,82 @@ impl DisplayData {
}
}
fn is_biggest(&self, num_siblings: u64, max_siblings: u64) -> bool {
fn is_biggest(&self, num_siblings: usize, max_siblings: u64) -> bool {
if self.is_reversed {
num_siblings == 0
num_siblings == (max_siblings - 1) as usize
} else {
num_siblings == max_siblings - 1
num_siblings == 0
}
}
fn get_children_from_node(&self, node: Node) -> impl Iterator<Item = Node> {
fn is_last(&self, num_siblings: usize, max_siblings: u64) -> bool {
if self.is_reversed {
let n: Vec<Node> = node.children.into_iter().rev().map(|a| a).collect();
n.into_iter()
num_siblings == 0
} else {
node.children.into_iter()
num_siblings == (max_siblings - 1) as usize
}
}
fn percent_size(&self, node: &Node) -> f32 {
let result = node.size as f32 / self.base_size as f32;
if result.is_normal() {
result
} else {
0.0
}
}
}
fn get_children_from_node(node: Node, is_reversed: bool) -> impl Iterator<Item = Node> {
if is_reversed {
let n: Vec<Node> = node.children.into_iter().rev().map(|a| a).collect();
n.into_iter()
} else {
node.children.into_iter()
}
}
struct DrawData<'a> {
indent: String,
percent_bar: String,
display_data: &'a DisplayData,
}
impl DrawData<'_> {
fn get_new_indent(&self, has_children: bool, was_i_last: bool) -> String {
let chars = self.display_data.get_tree_chars(was_i_last, has_children);
self.indent.to_string() + chars
}
fn generate_bar(&self, node: &Node, level: usize) -> String {
let chars_in_bar = self.percent_bar.chars().count();
let num_bars = chars_in_bar as f32 * self.display_data.percent_size(node);
let mut num_not_my_bar = (chars_in_bar as i32) - num_bars as i32;
let mut new_bar = "".to_string();
let idx = 5 - min(5, max(1, level));
for c in self.percent_bar.chars() {
num_not_my_bar -= 1;
if num_not_my_bar <= 0 {
new_bar.push(BLOCKS[0]);
} else if c == BLOCKS[0] {
new_bar.push(BLOCKS[idx]);
} else {
new_bar.push(c);
}
}
new_bar
}
}
fn get_width_of_terminal() -> u16 {
// Windows CI runners detect a very low terminal width
if let Some((Width(w), Height(_h))) = terminal_size() {
max(w, DEFAULT_TERMINAL_WIDTH)
} else {
DEFAULT_TERMINAL_WIDTH
}
}
pub fn draw_it(
@@ -78,44 +138,91 @@ pub fn draw_it(
use_full_path: bool,
is_reversed: bool,
no_colors: bool,
no_percents: bool,
root_node: Node,
) {
if !permissions {
eprintln!("Did not have permissions for all directories");
}
let display_data = DisplayData {
short_paths: !use_full_path,
is_reversed,
colors_on: !no_colors,
let longest_string_length = root_node
.children
.iter()
.map(|c| find_longest_dir_name(&c, " ", !use_full_path))
.fold(0, max);
let terminal_width = get_width_of_terminal() - 16;
let max_bar_length = if no_percents || longest_string_length >= terminal_width as usize {
0
} else {
terminal_width as usize - longest_string_length
};
for c in display_data.get_children_from_node(root_node) {
let first_tree_chars = display_data.get_first_chars();
display_node(c, true, first_tree_chars, &display_data)
// handle usize error also add do not show fancy output option
let bar_text = repeat(BLOCKS[0]).take(max_bar_length).collect::<String>();
for c in get_children_from_node(root_node, is_reversed) {
let display_data = DisplayData {
short_paths: !use_full_path,
is_reversed,
colors_on: !no_colors,
base_size: c.size,
longest_string_length,
};
let draw_data = DrawData {
indent: "".to_string(),
percent_bar: bar_text.clone(),
display_data: &display_data,
};
display_node(c, &draw_data, true, true);
}
}
fn display_node(node: Node, is_biggest: bool, indent: &str, display_data: &DisplayData) {
let mut num_siblings = node.children.len() as u64;
let max_sibling = num_siblings;
let new_indent = clean_indentation_string(indent);
let name = node.name.clone();
let size = node.size;
// We can probably pass depth instead of indent here.
// It is ugly to feed in ' ' instead of the actual tree characters but we don't need them yet.
fn find_longest_dir_name(node: &Node, indent: &str, long_paths: bool) -> usize {
let longest = UnicodeWidthStr::width(&*get_printable_name(&node.name, long_paths, indent));
if !display_data.is_reversed {
print_this_node(&*name, size, is_biggest, display_data, indent);
// each none root tree drawing is 2 chars
let full_indent: String = indent.to_string() + " ";
node.children
.iter()
.map(|c| find_longest_dir_name(c, &*full_indent, long_paths))
.fold(longest, max)
}
fn display_node(node: Node, draw_data: &DrawData, is_biggest: bool, is_last: bool) {
let indent2 = draw_data.get_new_indent(!node.children.is_empty(), is_last);
// hacky way of working out how deep we are in the tree
let level = ((indent2.chars().count() - 1) / 2) - 1;
let bar_text = draw_data.generate_bar(&node, level);
let to_print = format_string(
&node,
&*indent2,
&*bar_text,
is_biggest,
draw_data.display_data,
);
if !draw_data.display_data.is_reversed {
println!("{}", to_print)
}
for c in display_data.get_children_from_node(node) {
num_siblings -= 1;
let chars = display_data.get_tree_chars(num_siblings, max_sibling, !c.children.is_empty());
let is_biggest = display_data.is_biggest(num_siblings, max_sibling);
let full_indent = new_indent.clone() + chars;
display_node(c, is_biggest, &*full_indent, display_data)
let dd = DrawData {
indent: clean_indentation_string(&*indent2),
percent_bar: bar_text,
display_data: draw_data.display_data,
};
let num_siblings = node.children.len() as u64;
for (count, c) in get_children_from_node(node, draw_data.display_data.is_reversed).enumerate() {
let is_biggest = dd.display_data.is_biggest(count, num_siblings);
let was_i_last = dd.display_data.is_last(count, num_siblings);
display_node(c, &dd, is_biggest, was_i_last);
}
if display_data.is_reversed {
print_this_node(&*name, size, is_biggest, display_data, indent);
if draw_data.display_data.is_reversed {
println!("{}", to_print)
}
}
@@ -136,46 +243,59 @@ fn clean_indentation_string(s: &str) -> String {
is
}
fn print_this_node(
name: &str,
size: u64,
is_biggest: bool,
display_data: &DisplayData,
indentation: &str,
) {
let pretty_size = format!("{:>5}", human_readable_number(size),);
println!(
"{}",
format_string(name, is_biggest, display_data, &*pretty_size, indentation)
)
}
pub fn format_string(
dir_name: &str,
is_biggest: bool,
display_data: &DisplayData,
size: &str,
indentation: &str,
) -> String {
fn get_printable_name<P: AsRef<Path>>(dir_name: &P, long_paths: bool, indentation: &str) -> String {
let dir_name = dir_name.as_ref();
let printable_name = {
if display_data.short_paths {
dir_name
.split(std::path::is_separator)
.last()
.unwrap_or(dir_name)
if long_paths {
match dir_name.parent() {
Some(prefix) => match dir_name.strip_prefix(prefix) {
Ok(base) => base,
Err(_) => dir_name,
},
None => dir_name,
}
} else {
dir_name
}
};
format!("{} {}", indentation, printable_name.display())
}
pub fn format_string(
node: &Node,
indent: &str,
percent_bar: &str,
is_biggest: bool,
display_data: &DisplayData,
) -> String {
let pretty_size = format!("{:>5}", human_readable_number(node.size));
let percent_size = display_data.percent_size(node);
let percent_size_str = format!("{:.0}%", percent_size * 100.0);
let tree_and_path = get_printable_name(&node.name, display_data.short_paths, &*indent);
let printable_chars = UnicodeWidthStr::width(&*tree_and_path);
let tree_and_path = tree_and_path
+ &(repeat(" ")
.take(display_data.longest_string_length - printable_chars)
.collect::<String>());
let percents = if percent_bar != "" {
format!("{}{:>4}", percent_bar, percent_size_str)
} else {
"".into()
};
format!(
"{} {} {}",
"{} {}{}",
if is_biggest && display_data.colors_on {
Fixed(196).paint(size)
Fixed(196).paint(pretty_size)
} else {
Style::new().paint(size)
Style::new().paint(pretty_size)
},
indentation,
printable_name,
tree_and_path,
percents,
)
}
+36 -13
View File
@@ -1,15 +1,19 @@
#[macro_use]
extern crate clap;
extern crate unicode_width;
use self::display::draw_it;
use crate::utils::is_a_parent_of;
use clap::{App, AppSettings, Arg};
use std::cmp::max;
use std::path::PathBuf;
use terminal_size::{terminal_size, Height, Width};
use utils::{find_big_ones, get_dir_tree, simplify_dir_names, sort, trim_deep_ones, Node};
mod display;
mod utils;
static DEFAULT_NUMBER_OF_LINES: usize = 20;
static DEFAULT_NUMBER_OF_LINES: usize = 30;
#[cfg(windows)]
fn init_color() {
@@ -19,9 +23,21 @@ fn init_color() {
#[cfg(not(windows))]
fn init_color() {}
fn get_height_of_terminal() -> usize {
// Windows CI runners detect a terminal height of 0
if let Some((Width(_w), Height(h))) = terminal_size() {
max(h as usize, DEFAULT_NUMBER_OF_LINES) - 10
} else {
DEFAULT_NUMBER_OF_LINES - 10
}
}
fn main() {
init_color();
let def_num_str = DEFAULT_NUMBER_OF_LINES.to_string();
let default_height = get_height_of_terminal();
let def_num_str = default_height.to_string();
let options = App::new("Dust")
.about("Like du but more intuitive")
.version(crate_version!())
@@ -61,7 +77,7 @@ fn main() {
.takes_value(true)
.number_of_values(1)
.multiple(true)
.help("Exclude any file or directory with contains this substring."),
.help("Exclude any file or directory with this name."),
)
.arg(
Arg::with_name("limit_filesystem")
@@ -79,7 +95,7 @@ fn main() {
Arg::with_name("reverse")
.short("r")
.long("reverse")
.help("If applied tree will be printed upside down (biggest lowest)"),
.help("If applied tree will be printed upside down (biggest highest)"),
)
.arg(
Arg::with_name("no_colors")
@@ -87,6 +103,12 @@ fn main() {
.long("no_colors")
.help("If applied no colors will be printed (normally largest directories are marked in red"),
)
.arg(
Arg::with_name("no_bars")
.short("b")
.long("no_percent_bars")
.help("If applied no percent bars or percents will be displayed"),
)
.arg(Arg::with_name("inputs").multiple(true))
.get_matches();
@@ -101,7 +123,7 @@ fn main() {
Ok(v) => v,
Err(_) => {
eprintln!("Ignoring bad value for number_of_lines");
DEFAULT_NUMBER_OF_LINES
default_height
}
};
@@ -130,19 +152,22 @@ fn main() {
.map_err(|_| eprintln!("Ignoring bad value for depth"))
.ok()
});
if options.is_present("depth") && number_of_lines != DEFAULT_NUMBER_OF_LINES {
if options.is_present("depth") && number_of_lines != default_height {
eprintln!("Use either -n or -d. Not both");
return;
}
let use_apparent_size = options.is_present("display_apparent_size");
let limit_filesystem = options.is_present("limit_filesystem");
let ignore_directories = options.values_of("ignore_directory").map(|r| r.collect());
let ignore_directories = match options.values_of("ignore_directory") {
Some(i) => Some(i.map(PathBuf::from).collect()),
None => None,
};
let simplified_dirs = simplify_dir_names(target_dirs);
let (permissions, nodes) = get_dir_tree(
&simplified_dirs,
ignore_directories,
&ignore_directories,
use_apparent_size,
limit_filesystem,
threads,
@@ -159,13 +184,14 @@ fn main() {
draw_it(
permissions,
options.is_present("display_full_paths"),
options.is_present("reverse"),
!options.is_present("reverse"),
options.is_present("no_colors"),
options.is_present("no_bars"),
tree,
);
}
fn build_tree(biggest_ones: Vec<(String, u64)>, depth: Option<u64>) -> Node {
fn build_tree(biggest_ones: Vec<(PathBuf, u64)>, depth: Option<u64>) -> Node {
let mut top_parent = Node::default();
// assume sorted order
@@ -196,6 +222,3 @@ fn recursively_build_tree(parent_node: &mut Node, new_node: Node, depth: Option<
parent_node.children.push(new_node);
}
}
#[cfg(test)]
mod tests;
View File
-412
View File
@@ -1,412 +0,0 @@
use super::*;
use crate::display::DisplayData;
use display::format_string;
use std::fs::File;
use std::io::Write;
use std::panic;
use std::path::PathBuf;
use std::process::Command;
use tempfile::Builder;
use tempfile::TempDir;
// fix! [rivy; 2020-22-01] "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_main() {
assert_cli::Assert::main_binary()
.with_args(&["src/test_dir"])
.stdout()
.is(main_output(true).as_str())
.unwrap();
}
// fix! [rivy; 2020-22-01] "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_main_long_paths() {
assert_cli::Assert::main_binary()
.with_args(&["-p", "src/test_dir"])
.stdout()
.is(main_output(false).as_str())
.unwrap();
}
// fix! [rivy; 2020-22-01] "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_main_multi_arg() {
assert_cli::Assert::main_binary()
.with_args(&["src/test_dir/many/", "src/test_dir/", "src/test_dir"])
.stdout()
.is(main_output(true).as_str())
.unwrap();
}
#[cfg(target_os = "macos")]
fn main_output(short_paths: bool) -> String {
let d = DisplayData {
short_paths,
is_reversed: false,
colors_on: true,
};
format!(
"{}
{}
{}
{}",
format_string("src/test_dir", true, &d, " 4.0K", "─┬"),
format_string("src/test_dir/many", true, &d, " 4.0K", " └─┬",),
format_string("src/test_dir/many/hello_file", true, &d, " 4.0K", " ├──",),
format_string("src/test_dir/many/a_file", false, &d, " 0B", " └──",),
)
}
#[cfg(target_os = "linux")]
fn main_output(short_paths: bool) -> String {
let d = DisplayData {
short_paths,
is_reversed: false,
colors_on: true,
};
format!(
"{}
{}
{}
{}",
format_string("src/test_dir", true, &d, " 12K", "─┬"),
format_string("src/test_dir/many", true, &d, " 8.0K", " └─┬",),
format_string("src/test_dir/many/hello_file", true, &d, " 4.0K", " ├──",),
format_string("src/test_dir/many/a_file", false, &d, " 0B", " └──",),
)
}
#[cfg(target_os = "windows")]
fn main_output(short_paths: bool) -> String {
let d = DisplayData {
short_paths,
is_reversed: false,
colors_on: true,
};
format!(
"{}
{}
{}
{}",
format_string("src/test_dir", true, &d, " 6B", "─┬"),
format_string("src/test_dir\\many", true, &d, " 6B", " └─┬",),
format_string(
"src/test_dir\\many\\hello_file",
true,
&d,
" 6B",
" ├──",
),
format_string("src/test_dir\\many\\a_file", false, &d, " 0B", " └──",),
)
}
// fix! [rivy; 2020-22-01] "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_no_color_flag() {
assert_cli::Assert::main_binary()
.with_args(&["-c", "src/test_dir/"])
.stdout()
.is(no_color_flag_output().as_str())
.unwrap();
}
#[cfg(target_os = "macos")]
fn no_color_flag_output() -> String {
"
4.0K ─┬ test_dir
4.0K └─┬ many
4.0K ├── hello_file
0B └── a_file
"
.to_string()
}
#[cfg(target_os = "linux")]
fn no_color_flag_output() -> String {
"
12K ─┬ test_dir
8.0K └─┬ many
4.0K ├── hello_file
0B └── a_file
"
.to_string()
}
#[cfg(target_os = "windows")]
fn no_color_flag_output() -> String {
"
6B ─┬ test_dir
6B └─┬ many
6B ├── hello_file
0B └── a_file
"
.to_string()
}
// fix! [rivy; 2020-22-01] "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_apparent_size() {
let d = DisplayData {
short_paths: true,
is_reversed: false,
colors_on: true,
};
let r = format!(
"{}",
format_string("src/test_dir/many/hello_file", true, &d, " 6B", " ├──",),
);
assert_cli::Assert::main_binary()
.with_args(&["-s", "src/test_dir"])
.stdout()
.contains(r.as_str())
.unwrap();
}
#[test]
pub fn test_reverse_flag() {
// variable names the same length make the output easier to read
let a = " ┌── a_file";
let b = " ├── hello_file";
let c = " ┌─┴ many";
let d = " ─┴ test_dir";
assert_cli::Assert::main_binary()
.with_args(&["-r", "src/test_dir"])
.stdout()
.contains(a)
.stdout()
.contains(b)
.stdout()
.contains(c)
.stdout()
.contains(d)
.unwrap();
}
#[test]
pub fn test_d_flag_works() {
// We should see the top level directory but not the sub dirs / files:
assert_cli::Assert::main_binary()
.with_args(&["-d", "1", "-s", "src/test_dir"])
.stdout()
.doesnt_contain("hello_file")
.unwrap();
}
fn build_temp_file(dir: &TempDir) -> PathBuf {
let file_path = dir.path().join("notes.txt");
let mut file = File::create(&file_path).unwrap();
writeln!(file, "I am a temp file").unwrap();
file_path
}
// fix! [rivy; 2020-01-22] possible on "windows"?; `ln` is not usually an available command; creation of symbolic links requires special enhanced permissions
// ... ref: <https://superuser.com/questions/343074/directory-junction-vs-directory-symbolic-link> @@ <https://archive.is/gpTLE>
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_soft_sym_link() {
let dir = Builder::new().tempdir().unwrap();
let file = build_temp_file(&dir);
let dir_s = dir.path().to_str().unwrap();
let file_path_s = file.to_str().unwrap();
let link_name = dir.path().join("the_link");
let link_name_s = link_name.to_str().unwrap();
let c = Command::new("ln")
.arg("-s")
.arg(file_path_s)
.arg(link_name_s)
.output();
assert!(c.is_ok());
let a = format!(" ─┬ {}", dir_s);
let b = format!(" ├── {}", file_path_s);
let c = format!(" └── {}", link_name_s);
assert_cli::Assert::main_binary()
.with_args(&["-p", &dir_s])
.stdout()
.contains(a.as_str())
.stdout()
.contains(b.as_str())
.stdout()
.contains(c.as_str())
.unwrap();
}
// Hard links are ignored as the inode is the same as the file
// fix! [rivy; 2020-01-22] may fail on "usual" windows hosts as `ln` is not usually an available command
#[test]
pub fn test_hard_sym_link() {
let dir = Builder::new().tempdir().unwrap();
let file = build_temp_file(&dir);
let dir_s = dir.path().to_str().unwrap();
let file_path_s = file.to_str().unwrap();
let link_name = dir.path().join("the_link");
let link_name_s = link_name.to_str().unwrap();
let c = Command::new("ln")
.arg(file_path_s)
.arg(link_name_s)
.output();
assert!(c.is_ok());
let a = format!(" ─┬ {}", dir_s);
let b = format!(" └── {}", link_name_s);
let b2 = format!(" └── {}", file_path_s);
// Because this is a hard link the file and hard link look identical. Therefore
// we cannot guarantee which version will appear first.
let result = panic::catch_unwind(|| {
assert_cli::Assert::main_binary()
.with_args(&["-p", dir_s])
.stdout()
.contains(a.as_str())
.stdout()
.contains(b.as_str())
.unwrap();
});
if result.is_err() {
assert_cli::Assert::main_binary()
.with_args(&["-p", dir_s])
.stdout()
.contains(a.as_str())
.stdout()
.contains(b2.as_str())
.unwrap();
}
}
// Check we don't recurse down an infinite symlink tree
// fix! [rivy; 2020-01-22] possible on "windows"?; `ln` is not usually an available command; creation of symbolic links requires special enhanced permissions
// ... ref: <https://superuser.com/questions/343074/directory-junction-vs-directory-symbolic-link> @@ <https://archive.is/gpTLE>
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_recursive_sym_link() {
let dir = Builder::new().tempdir().unwrap();
let dir_s = dir.path().to_str().unwrap();
let link_name = dir.path().join("the_link");
let link_name_s = link_name.to_str().unwrap();
let c = Command::new("ln")
.arg("-s")
.arg(dir_s)
.arg(link_name_s)
.output();
assert!(c.is_ok());
let a = format!(" ─┬ {}", dir_s);
let b = format!(" └── {}", link_name_s);
assert_cli::Assert::main_binary()
.with_args(&["-p", dir_s])
.stdout()
.contains(a.as_str())
.stdout()
.contains(b.as_str())
.unwrap();
}
// Check against directories and files whos names are substrings of each other
// fix! [rivy; 2020-22-01] "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_substring_of_names() {
assert_cli::Assert::main_binary()
.with_args(&["-c", "src/test_dir2"])
.stdout()
.is(no_substring_of_names_output().as_str())
.unwrap();
}
#[cfg(target_os = "linux")]
fn no_substring_of_names_output() -> String {
"
24K ─┬ test_dir2
8.0K ├─┬ dir
4.0K │ └── hello
8.0K ├─┬ dir_substring
4.0K │ └── hello
4.0K └── dir_name_clash
"
.into()
}
#[cfg(target_os = "macos")]
fn no_substring_of_names_output() -> String {
"
12K ─┬ test_dir2
4.0K ├─┬ dir
4.0K │ └── hello
4.0K ├── dir_name_clash
4.0K └─┬ dir_substring
4.0K └── hello
"
.into()
}
#[cfg(target_os = "windows")]
fn no_substring_of_names_output() -> String {
"
16B ─┬ test_dir2
6B ├─┬ dir_substring
6B │ └── hello
5B ├─┬ dir
5B │ └── hello
5B └── dir_name_clash
"
.into()
}
// Check against directories and files whos names are substrings of each other
#[test]
pub fn test_ignore_dir() {
assert_cli::Assert::main_binary()
.with_args(&["-c", "-X", "dir_substring", "src/test_dir2"])
.stdout()
.is(ignore_dir_output().as_str())
.unwrap();
}
#[cfg(target_os = "linux")]
fn ignore_dir_output() -> String {
"
16K ─┬ test_dir2
8.0K ├─┬ dir
4.0K │ └── hello
4.0K └── dir_name_clash
"
.into()
}
#[cfg(target_os = "macos")]
fn ignore_dir_output() -> String {
"
8.0K ─┬ test_dir2
4.0K ├─┬ dir
4.0K │ └── hello
4.0K └── dir_name_clash
"
.into()
}
#[cfg(target_os = "windows")]
fn ignore_dir_output() -> String {
"
10B ─┬ test_dir2
5B ├─┬ dir
5B │ └── hello
5B └── dir_name_clash
"
.into()
}
+124 -104
View File
@@ -2,6 +2,7 @@ use jwalk::DirEntry;
use std::cmp::Ordering;
use std::collections::HashMap;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use jwalk::WalkDir;
@@ -10,7 +11,7 @@ use self::platform::*;
#[derive(Debug, Default, Eq)]
pub struct Node {
pub name: String,
pub name: PathBuf,
pub size: u64,
pub children: Vec<Node>,
}
@@ -37,15 +38,15 @@ impl PartialEq for Node {
}
}
pub fn is_a_parent_of(parent: &str, child: &str) -> bool {
let path_parent = std::path::Path::new(parent);
let path_child = std::path::Path::new(child);
(path_child.starts_with(path_parent) && !path_parent.starts_with(path_child))
pub fn is_a_parent_of<P: AsRef<Path>>(parent: P, child: P) -> bool {
let parent = parent.as_ref();
let child = child.as_ref();
(child.starts_with(parent) && !parent.starts_with(child))
}
pub fn simplify_dir_names(filenames: Vec<&str>) -> HashSet<String> {
let mut top_level_names: HashSet<String> = HashSet::with_capacity(filenames.len());
let mut to_remove: Vec<String> = Vec::with_capacity(filenames.len());
pub fn simplify_dir_names<P: AsRef<Path>>(filenames: Vec<P>) -> HashSet<PathBuf> {
let mut top_level_names: HashSet<PathBuf> = HashSet::with_capacity(filenames.len());
let mut to_remove: Vec<PathBuf> = Vec::with_capacity(filenames.len());
for t in filenames {
let top_level_name = normalize_path(t);
@@ -53,7 +54,7 @@ pub fn simplify_dir_names(filenames: Vec<&str>) -> HashSet<String> {
for tt in top_level_names.iter() {
if is_a_parent_of(&top_level_name, tt) {
to_remove.push(tt.to_string());
to_remove.push(tt.to_path_buf());
} else if is_a_parent_of(tt, &top_level_name) {
can_add = false;
}
@@ -62,22 +63,22 @@ pub fn simplify_dir_names(filenames: Vec<&str>) -> HashSet<String> {
top_level_names.retain(|tr| to_remove.binary_search(tr).is_err());
to_remove.clear();
if can_add {
top_level_names.insert(normalize_path(t).to_owned());
top_level_names.insert(top_level_name);
}
}
top_level_names
}
pub fn get_dir_tree(
top_level_names: &HashSet<String>,
ignore_directories: Option<Vec<&str>>,
pub fn get_dir_tree<P: AsRef<Path>>(
top_level_names: &HashSet<P>,
ignore_directories: &Option<Vec<PathBuf>>,
apparent_size: bool,
limit_filesystem: bool,
threads: Option<usize>,
) -> (bool, HashMap<String, u64>) {
) -> (bool, HashMap<PathBuf, u64>) {
let mut permissions = 0;
let mut data: HashMap<String, u64> = HashMap::new();
let mut data: HashMap<PathBuf, u64> = HashMap::new();
let restricted_filesystems = if limit_filesystem {
get_allowed_filesystems(top_level_names)
} else {
@@ -86,10 +87,10 @@ pub fn get_dir_tree(
for b in top_level_names.iter() {
examine_dir(
&b,
b,
apparent_size,
&restricted_filesystems,
&ignore_directories,
ignore_directories,
&mut data,
&mut permissions,
threads,
@@ -98,7 +99,7 @@ pub fn get_dir_tree(
(permissions == 0, data)
}
fn get_allowed_filesystems(top_level_names: &HashSet<String>) -> Option<HashSet<u64>> {
fn get_allowed_filesystems<P: AsRef<Path>>(top_level_names: &HashSet<P>) -> Option<HashSet<u64>> {
let mut limit_filesystems: HashSet<u64> = HashSet::new();
for file_name in top_level_names.iter() {
if let Ok(a) = get_filesystem(file_name) {
@@ -108,29 +109,26 @@ fn get_allowed_filesystems(top_level_names: &HashSet<String>) -> Option<HashSet<
Some(limit_filesystems)
}
pub fn normalize_path<P: AsRef<std::path::Path>>(path: P) -> std::string::String {
pub fn normalize_path<P: AsRef<Path>>(path: P) -> PathBuf {
// normalize path ...
// 1. removing repeated separators
// 2. removing interior '.' ("current directory") path segments
// 3. removing trailing extra separators and '.' ("current directory") path segments
// * `Path.components()` does all the above work; ref: <https://doc.rust-lang.org/std/path/struct.Path.html#method.components>
// 4. changing to os preferred separator (automatically done by recollecting components back into a PathBuf)
path.as_ref()
.components()
.collect::<std::path::PathBuf>()
.to_string_lossy()
.to_string()
path.as_ref().components().collect::<PathBuf>()
}
fn examine_dir(
top_dir: &str,
fn examine_dir<P: AsRef<Path>>(
top_dir: P,
apparent_size: bool,
filesystems: &Option<HashSet<u64>>,
ignore_directories: &Option<Vec<&str>>,
data: &mut HashMap<String, u64>,
ignore_directories: &Option<Vec<PathBuf>>,
data: &mut HashMap<PathBuf, u64>,
file_count_no_permission: &mut u64,
threads: Option<usize>,
) {
let top_dir = top_dir.as_ref();
let mut inodes: HashSet<(u64, u64)> = HashSet::new();
let mut iter = WalkDir::new(top_dir)
.preload_metadata(true)
@@ -141,17 +139,23 @@ fn examine_dir(
'entry: for entry in iter {
if let Ok(e) = entry {
let maybe_size_and_inode = get_metadata(&e, apparent_size);
if let Some(d) = ignore_directories {
for s in d {
if e.path().to_string_lossy().contains(*s) {
if let Some(dirs) = ignore_directories {
let path = e.path();
let parts = path.components().collect::<Vec<std::path::Component>>();
for d in dirs {
let seq = d.components().collect::<Vec<std::path::Component>>();
if parts
.windows(seq.len())
.any(|window| window.iter().collect::<PathBuf>() == *d)
{
continue 'entry;
}
}
}
match maybe_size_and_inode {
Some((size, maybe_inode)) => {
if !should_ignore_file(apparent_size, filesystems, &mut inodes, maybe_inode) {
Some((size, inode, device)) => {
if !should_ignore_file(apparent_size, filesystems, &mut inodes, inode, device) {
process_file_with_size_and_inode(top_dir, data, e, size)
}
}
@@ -167,52 +171,49 @@ fn should_ignore_file(
apparent_size: bool,
restricted_filesystems: &Option<HashSet<u64>>,
inodes: &mut HashSet<(u64, u64)>,
maybe_inode: Option<(u64, u64)>,
inode: u64,
device: u64,
) -> bool {
if !apparent_size {
if let Some(inode_dev_pair) = maybe_inode {
// Ignore files on different devices (if flag applied)
if restricted_filesystems.is_some()
&& !restricted_filesystems
.as_ref()
.unwrap()
.contains(&inode_dev_pair.1)
{
return true;
}
// Ignore files already visited or symlinked
if inodes.contains(&inode_dev_pair) {
return true;
}
inodes.insert(inode_dev_pair);
// Ignore files on different devices (if flag applied)
if let Some(rs) = restricted_filesystems {
if !rs.contains(&device) {
return true;
}
}
if !apparent_size {
// Ignore files already visited or symlinked
if inodes.contains(&(inode, device)) {
return true;
}
inodes.insert((inode, device));
}
false
}
fn process_file_with_size_and_inode(
top_dir: &str,
data: &mut HashMap<String, u64>,
fn process_file_with_size_and_inode<P: AsRef<Path>>(
top_dir: P,
data: &mut HashMap<PathBuf, u64>,
e: DirEntry,
size: u64,
) {
let top_dir = top_dir.as_ref();
// This path and all its parent paths have their counter incremented
for path_name in e.path().ancestors() {
for path in e.path().ancestors() {
// This is required due to bug in Jwalk that adds '/' to all sub dir lists
// see: https://github.com/jessegrosjean/jwalk/issues/13
if path_name.to_string_lossy() == "/" && top_dir != "/" {
if path.to_string_lossy() == "/" && top_dir.to_string_lossy() != "/" {
continue;
}
let path_name = path_name.to_string_lossy();
let s = data.entry(path_name.to_string()).or_insert(0);
let s = data.entry(normalize_path(path)).or_insert(0);
*s += size;
if path_name == top_dir {
if path.starts_with(top_dir) && top_dir.starts_with(path) {
break;
}
}
}
pub fn sort_by_size_first_name_second(a: &(String, u64), b: &(String, u64)) -> Ordering {
pub fn sort_by_size_first_name_second(a: &(PathBuf, u64), b: &(PathBuf, u64)) -> Ordering {
let result = b.1.cmp(&a.1);
if result == Ordering::Equal {
a.0.cmp(&b.0)
@@ -221,13 +222,13 @@ pub fn sort_by_size_first_name_second(a: &(String, u64), b: &(String, u64)) -> O
}
}
pub fn sort(data: HashMap<String, u64>) -> Vec<(String, u64)> {
let mut new_l: Vec<(String, u64)> = data.iter().map(|(a, b)| (a.clone(), *b)).collect();
pub fn sort(data: HashMap<PathBuf, u64>) -> Vec<(PathBuf, u64)> {
let mut new_l: Vec<(PathBuf, u64)> = data.iter().map(|(a, b)| (a.clone(), *b)).collect();
new_l.sort_unstable_by(sort_by_size_first_name_second);
new_l
}
pub fn find_big_ones(new_l: Vec<(String, u64)>, max_to_show: usize) -> Vec<(String, u64)> {
pub fn find_big_ones(new_l: Vec<(PathBuf, u64)>, max_to_show: usize) -> Vec<(PathBuf, u64)> {
if max_to_show > 0 && new_l.len() > max_to_show {
new_l[0..max_to_show].to_vec()
} else {
@@ -235,20 +236,29 @@ pub fn find_big_ones(new_l: Vec<(String, u64)>, max_to_show: usize) -> Vec<(Stri
}
}
fn depth_of_path(name: &PathBuf) -> usize {
// Filter required as paths can have some odd preliminary
// ("Prefix") bits (for example, from windows, "\\?\" or "\\UNC\")
name.components()
.filter(|&c| match c {
std::path::Component::Prefix(_) => false,
_ => true,
})
.count()
}
pub fn trim_deep_ones(
input: Vec<(String, u64)>,
input: Vec<(PathBuf, u64)>,
max_depth: u64,
top_level_names: &HashSet<String>,
) -> Vec<(String, u64)> {
let mut result: Vec<(String, u64)> = Vec::with_capacity(input.len() * top_level_names.len());
top_level_names: &HashSet<PathBuf>,
) -> Vec<(PathBuf, u64)> {
let mut result: Vec<(PathBuf, u64)> = Vec::with_capacity(input.len() * top_level_names.len());
for name in top_level_names {
let my_max_depth = name.matches(std::path::is_separator).count() + max_depth as usize;
let name_ref: &str = name.as_ref();
let my_max_depth = depth_of_path(name) + max_depth as usize;
for &(ref k, ref v) in input.iter() {
if k.starts_with(name_ref) && k.matches(std::path::is_separator).count() <= my_max_depth
{
if k.starts_with(name) && depth_of_path(k) <= my_max_depth {
result.push((k.clone(), *v));
}
}
@@ -263,34 +273,22 @@ mod tests {
#[test]
fn test_simplify_dir() {
let mut correct = HashSet::new();
correct.insert("a".to_string());
correct.insert(PathBuf::from("a"));
assert_eq!(simplify_dir_names(vec!["a"]), correct);
}
#[test]
fn test_simplify_dir_rm_subdir() {
let mut correct = HashSet::new();
correct.insert(
["a", "b"]
.iter()
.collect::<std::path::PathBuf>()
.to_string_lossy()
.to_string(),
);
correct.insert(["a", "b"].iter().collect::<PathBuf>());
assert_eq!(simplify_dir_names(vec!["a/b", "a/b/c", "a/b/d/f"]), correct);
}
#[test]
fn test_simplify_dir_duplicates() {
let mut correct = HashSet::new();
correct.insert(
["a", "b"]
.iter()
.collect::<std::path::PathBuf>()
.to_string_lossy()
.to_string(),
);
correct.insert("c".to_string());
correct.insert(["a", "b"].iter().collect::<PathBuf>());
correct.insert(PathBuf::from("c"));
assert_eq!(
simplify_dir_names(vec![
"a/b",
@@ -308,36 +306,24 @@ mod tests {
#[test]
fn test_simplify_dir_rm_subdir_and_not_substrings() {
let mut correct = HashSet::new();
correct.insert("b".to_string());
correct.insert(
["c", "a", "b"]
.iter()
.collect::<std::path::PathBuf>()
.to_string_lossy()
.to_string(),
);
correct.insert(
["a", "b"]
.iter()
.collect::<std::path::PathBuf>()
.to_string_lossy()
.to_string(),
);
correct.insert(PathBuf::from("b"));
correct.insert(["c", "a", "b"].iter().collect::<PathBuf>());
correct.insert(["a", "b"].iter().collect::<PathBuf>());
assert_eq!(simplify_dir_names(vec!["a/b", "c/a/b/", "b"]), correct);
}
#[test]
fn test_simplify_dir_dots() {
let mut correct = HashSet::new();
correct.insert("src".to_string());
correct.insert(PathBuf::from("src"));
assert_eq!(simplify_dir_names(vec!["src/."]), correct);
}
#[test]
fn test_simplify_dir_substring_names() {
let mut correct = HashSet::new();
correct.insert("src".to_string());
correct.insert("src_v2".to_string());
correct.insert(PathBuf::from("src"));
correct.insert(PathBuf::from("src_v2"));
assert_eq!(simplify_dir_names(vec!["src/", "src_v2"]), correct);
}
@@ -360,4 +346,38 @@ mod tests {
assert!(is_a_parent_of("/", "/usr"));
assert!(!is_a_parent_of("/", "/"));
}
#[test]
fn test_should_ignore_file() {
let mut files = HashSet::new();
files.insert((10, 20));
assert!(!should_ignore_file(true, &None, &mut files, 0, 0));
// New file is not known it will be inserted to the hashmp and should not be ignored
assert!(!should_ignore_file(false, &None, &mut files, 11, 12));
assert!(files.contains(&(11, 12)));
// The same file will be ignored the second time
assert!(should_ignore_file(false, &None, &mut files, 11, 12));
}
#[test]
fn test_should_ignore_file_on_different_device() {
let mut files = HashSet::new();
files.insert((10, 20));
let mut devices = HashSet::new();
devices.insert(99);
let od = Some(devices);
// If we are looking at a different device (disk) and the device flag is set
// then apparent_size is irrelevant - we ignore files on other devices
assert!(should_ignore_file(false, &od, &mut files, 11, 12));
assert!(should_ignore_file(true, &od, &mut files, 11, 12));
// We do not ignore files on the same device
assert!(!should_ignore_file(false, &od, &mut files, 2, 99));
assert!(!should_ignore_file(true, &od, &mut files, 2, 99));
}
}
+9 -18
View File
@@ -2,6 +2,7 @@ use jwalk::DirEntry;
#[allow(unused_imports)]
use std::fs;
use std::io;
use std::path::Path;
#[cfg(target_family = "unix")]
fn get_block_size() -> u64 {
@@ -11,20 +12,19 @@ fn get_block_size() -> u64 {
}
#[cfg(target_family = "unix")]
pub fn get_metadata(d: &DirEntry, use_apparent_size: bool) -> Option<(u64, Option<(u64, u64)>)> {
pub fn get_metadata(d: &DirEntry, use_apparent_size: bool) -> Option<(u64, u64, u64)> {
use std::os::unix::fs::MetadataExt;
d.metadata.as_ref().unwrap().as_ref().ok().map(|md| {
let inode = Some((md.ino(), md.dev()));
if use_apparent_size {
(md.len(), inode)
(md.len(), md.ino(), md.dev())
} else {
(md.blocks() * get_block_size(), inode)
(md.blocks() * get_block_size(), md.ino(), md.dev())
}
})
}
#[cfg(target_family = "windows")]
pub fn get_metadata(d: &DirEntry, _use_apparent_size: bool) -> Option<(u64, Option<(u64, u64)>)> {
pub fn get_metadata(d: &DirEntry, _use_apparent_size: bool) -> Option<(u64, u64, u64)> {
use winapi_util::file::information;
use winapi_util::Handle;
@@ -33,29 +33,20 @@ pub fn get_metadata(d: &DirEntry, _use_apparent_size: bool) -> Option<(u64, Opti
Some((
info.file_size(),
Some((info.file_index(), info.volume_serial_number())),
info.file_index(),
info.volume_serial_number(),
))
}
#[cfg(all(not(target_family = "windows"), not(target_family = "unix")))]
pub fn get_metadata(d: &DirEntry, _apparent: bool) -> Option<(u64, Option<(u64, u64)>)> {
d.metadata
.as_ref()
.unwrap()
.as_ref()
.ok()
.map(|md| (md.len(), None))
}
#[cfg(target_family = "unix")]
pub fn get_filesystem(file_path: &str) -> Result<u64, io::Error> {
pub fn get_filesystem<P: AsRef<Path>>(file_path: P) -> Result<u64, io::Error> {
use std::os::unix::fs::MetadataExt;
let metadata = fs::metadata(file_path)?;
Ok(metadata.dev())
}
#[cfg(target_family = "windows")]
pub fn get_filesystem(file_path: &str) -> Result<u64, io::Error> {
pub fn get_filesystem<P: AsRef<Path>>(file_path: P) -> Result<u64, io::Error> {
use winapi_util::file::information;
use winapi_util::Handle;
+259
View File
@@ -0,0 +1,259 @@
mod tests_symlinks;
// File sizes differ on both platform and on the format of the disk.
// We can at least test the file names are there
#[test]
pub fn test_basic_output() {
assert_cli::Assert::main_binary()
.with_args(&["src/test_dir/"])
.stdout()
.contains(" ┌─┴ test_dir ")
.stdout()
.contains(" ┌─┴ many ")
.stdout()
.contains(" ├── hello_file")
.stdout()
.contains(" ┌── a_file ")
.unwrap();
}
// fix! [rivy; 2020-22-01] "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_main_basic() {
// -c is no color mode - This makes testing much simpler
assert_cli::Assert::main_binary()
.with_args(&["-c", "src/test_dir"])
.stdout()
.is(main_output().as_str())
.unwrap();
}
// fix! [rivy; 2020-22-01] "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_main_multi_arg() {
assert_cli::Assert::main_binary()
.with_args(&["-c", "src/test_dir/many/", "src/test_dir/", "src/test_dir"])
.stdout()
.is(main_output().as_str())
.unwrap();
}
#[cfg(target_os = "macos")]
fn main_output() -> String {
r#"
0B ┌── a_file │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█ │ 0%
4.0K ├── hello_file│██████████████████████████████████████████████ │ 100%
4.0K ┌─┴ many │██████████████████████████████████████████████ │ 100%
4.0K ┌─┴ test_dir │██████████████████████████████████████████████ │ 100%
"#
.to_string()
}
#[cfg(target_os = "linux")]
fn main_output() -> String {
r#"
0B ┌── a_file │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█ │ 0%
4.0K ├── hello_file│ ░░░░░░░░░░░░░░░████████████████ │ 33%
8.0K ┌─┴ many │ ███████████████████████████████ │ 67%
12K ┌─┴ test_dir │██████████████████████████████████████████████ │ 100%
"#
.to_string()
}
#[cfg(target_os = "windows")]
fn main_output() -> String {
"PRs welcome".to_string()
}
// fix! [rivy; 2020-22-01] "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_main_long_paths() {
assert_cli::Assert::main_binary()
.with_args(&["-c", "-p", "src/test_dir"])
.stdout()
.is(main_output_long_paths().as_str())
.unwrap();
}
#[cfg(target_os = "macos")]
fn main_output_long_paths() -> String {
r#"
0B ┌── src/test_dir/many/a_file │░░░░░░░░░░░░░░░░░░░░░░░░░░░█ │ 0%
4.0K ├── src/test_dir/many/hello_file│████████████████████████████ │ 100%
4.0K ┌─┴ src/test_dir/many │████████████████████████████ │ 100%
4.0K ┌─┴ src/test_dir │████████████████████████████ │ 100%
"#
.to_string()
}
#[cfg(target_os = "linux")]
fn main_output_long_paths() -> String {
r#"
0B ┌── src/test_dir/many/a_file │ ░░░░░░░░░░░░░░░░░░█ │ 0%
4.0K ├── src/test_dir/many/hello_file│ ░░░░░░░░░██████████ │ 33%
8.0K ┌─┴ src/test_dir/many │ ███████████████████ │ 67%
12K ┌─┴ src/test_dir │████████████████████████████ │ 100%
"#
.to_string()
}
#[cfg(target_os = "windows")]
fn main_output_long_paths() -> String {
"PRs welcome".to_string()
}
// fix! [rivy; 2020-22-01] "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_apparent_size() {
assert_cli::Assert::main_binary()
.with_args(&["-c", "-s", "src/test_dir"])
.stdout()
.is(output_apparent_size().as_str())
.unwrap();
}
#[cfg(target_os = "linux")]
fn output_apparent_size() -> String {
r#"
0B ┌── a_file │ ░░░░░░░░░░░░░░░░░░░░░░░█ │ 0%
6B ├── hello_file│ ░░░░░░░░░░░░░░░░░░░░░░░█ │ 0%
4.0K ┌─┴ many │ ████████████████████████ │ 50%
8.0K ┌─┴ test_dir │██████████████████████████████████████████████ │ 100%
"#
.to_string()
}
#[cfg(target_os = "macos")]
fn output_apparent_size() -> String {
r#"
0B ┌── a_file │ ░░░░░░░░░░░░░░░░░░░░░░░░░░█ │ 0%
6B ├── hello_file│ ░░░░░░░░░░░░░░░░░░░░░░░░░██ │ 3%
134B ┌─┴ many │ ███████████████████████████ │ 58%
230B ┌─┴ test_dir │██████████████████████████████████████████████ │ 100%
"#
.to_string()
}
#[cfg(target_os = "windows")]
fn output_apparent_size() -> String {
"".to_string()
}
#[test]
pub fn test_reverse_flag() {
assert_cli::Assert::main_binary()
.with_args(&["-c", "-r", "src/test_dir/"])
.stdout()
.contains(" └─┬ test_dir ")
.stdout()
.contains(" └─┬ many ")
.stdout()
.contains(" ├── hello_file")
.stdout()
.contains(" └── a_file ")
.unwrap();
}
#[test]
pub fn test_d_flag_works() {
// We should see the top level directory but not the sub dirs / files:
assert_cli::Assert::main_binary()
.with_args(&["-d", "1", "-s", "src/test_dir"])
.stdout()
.doesnt_contain("hello_file")
.unwrap();
}
// Check against directories and files whos names are substrings of each other
// fix! [rivy; 2020-22-01] "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_substring_of_names() {
assert_cli::Assert::main_binary()
.with_args(&["-c", "src/test_dir2"])
.stdout()
.is(no_substring_of_names_output().as_str())
.unwrap();
}
#[cfg(target_os = "linux")]
fn no_substring_of_names_output() -> String {
"
4.0K ┌── dir_name_clash│ ████████ │ 17%
4.0K │ ┌── hello │ ░░░░░░░████████ │ 17%
8.0K ├─┴ dir_substring │ ███████████████ │ 33%
4.0K │ ┌── hello │ ░░░░░░░████████ │ 17%
8.0K ├─┴ dir │ ███████████████ │ 33%
24K ┌─┴ test_dir2 │████████████████████████████████████████████ │ 100%
"
.into()
}
#[cfg(target_os = "macos")]
fn no_substring_of_names_output() -> String {
"
4.0K ┌── hello │ ███████████████ │ 33%
4.0K ┌─┴ dir_substring │ ███████████████ │ 33%
4.0K ├── dir_name_clash│ ███████████████ │ 33%
4.0K │ ┌── hello │ ███████████████ │ 33%
4.0K ├─┴ dir │ ███████████████ │ 33%
12K ┌─┴ test_dir2 │████████████████████████████████████████████ │ 100%
"
.into()
}
#[cfg(target_os = "windows")]
fn no_substring_of_names_output() -> String {
"PRs".into()
}
// fix! [rivy; 2020-22-01] "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_unicode_directories() {
assert_cli::Assert::main_binary()
.with_args(&["-c", "src/test_dir3"])
.stdout()
.is(unicode_dir().as_str())
.unwrap();
}
#[cfg(target_os = "linux")]
fn unicode_dir() -> String {
// The way unicode & asian characters are rendered on the terminal should make this line up
"
0B ┌── 👩.unicode │ █ │ 0%
0B ├── ラウトは難しいです!.japan│ █ │ 0%
4.0K ┌─┴ test_dir3 │████████████████████████████████ │ 100%
"
.into()
}
#[cfg(target_os = "macos")]
fn unicode_dir() -> String {
"
0B ┌── 👩.unicode │ █ │ 0%
0B ├── ラウトは難しいです!.japan│ █ │ 0%
0B ┌─┴ test_dir3 │ █ │ 0%
"
.into()
}
#[cfg(target_os = "windows")]
fn unicode_dir() -> String {
"".into()
}
// Check against directories and files whos names are substrings of each other
#[test]
pub fn test_ignore_dir() {
assert_cli::Assert::main_binary()
.with_args(&["-c", "-X", "dir_substring", "src/test_dir2"])
.stdout()
.doesnt_contain("dir_substring")
.unwrap();
}
+119
View File
@@ -0,0 +1,119 @@
use std::fs::File;
use std::io::Write;
use std::panic;
use std::path::PathBuf;
use std::process::Command;
use tempfile::Builder;
use tempfile::TempDir;
// File sizes differ on both platform and on the format of the disk.
// Windows: `ln` is not usually an available command; creation of symbolic links requires special enhanced permissions
fn build_temp_file(dir: &TempDir) -> PathBuf {
let file_path = dir.path().join("notes.txt");
let mut file = File::create(&file_path).unwrap();
writeln!(file, "I am a temp file").unwrap();
file_path
}
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_soft_sym_link() {
let dir = Builder::new().tempdir().unwrap();
let file = build_temp_file(&dir);
let dir_s = dir.path().to_str().unwrap();
let file_path_s = file.to_str().unwrap();
let link_name = dir.path().join("the_link");
let link_name_s = link_name.to_str().unwrap();
let c = Command::new("ln")
.arg("-s")
.arg(file_path_s)
.arg(link_name_s)
.output();
assert!(c.is_ok());
let c = format!(" ┌── {}", link_name_s);
let b = format!(" ├── {}", file_path_s);
let a = format!("─┴ {}", dir_s);
assert_cli::Assert::main_binary()
.with_args(&["-p", &dir_s])
.stdout()
.contains(a.as_str())
.stdout()
.contains(b.as_str())
.stdout()
.contains(c.as_str())
.unwrap();
}
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_hard_sym_link() {
let dir = Builder::new().tempdir().unwrap();
let file = build_temp_file(&dir);
let dir_s = dir.path().to_str().unwrap();
let file_path_s = file.to_str().unwrap();
let link_name = dir.path().join("the_link");
let link_name_s = link_name.to_str().unwrap();
let c = Command::new("ln")
.arg(file_path_s)
.arg(link_name_s)
.output();
assert!(c.is_ok());
let a = format!("─┴ {}", dir_s);
let b = format!(" ┌── {}", link_name_s);
let b2 = format!(" ┌── {}", file_path_s);
// Because this is a hard link the file and hard link look identical. Therefore
// we cannot guarantee which version will appear first.
let result = panic::catch_unwind(|| {
assert_cli::Assert::main_binary()
.with_args(&["-p", dir_s])
.stdout()
.contains(a.as_str())
.stdout()
.contains(b.as_str())
.unwrap();
});
if result.is_err() {
assert_cli::Assert::main_binary()
.with_args(&["-p", dir_s])
.stdout()
.contains(a.as_str())
.stdout()
.contains(b2.as_str())
.unwrap();
}
}
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_recursive_sym_link() {
let dir = Builder::new().tempdir().unwrap();
let dir_s = dir.path().to_str().unwrap();
let link_name = dir.path().join("the_link");
let link_name_s = link_name.to_str().unwrap();
let c = Command::new("ln")
.arg("-s")
.arg(dir_s)
.arg(link_name_s)
.output();
assert!(c.is_ok());
let a = format!("─┬ {}", dir_s);
let b = format!(" └── {}", link_name_s);
assert_cli::Assert::main_binary()
.with_args(&["-r", "-p", dir_s])
.stdout()
.contains(a.as_str())
.stdout()
.contains(b.as_str())
.unwrap();
}